From 4581a9062289fe8f47a00b75977abf11fc68e0fc Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 15 Aug 2025 14:45:36 +1000 Subject: [PATCH 001/393] Bumping the sunset version to match the main project --- sftp/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index d585b97c..43385160 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -4,5 +4,5 @@ version = "0.1.0" edition = "2024" [dependencies] -sunset = { version = "0.2.0", path = "../" } +sunset = { version = "0.3.0", path = "../" } sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } From b6a94889face3ee35099b0406c7bc90fe04cd5b2 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 11:58:29 +1000 Subject: [PATCH 002/393] cargo fmt --- sftp/src/proto.rs | 7 +++++-- sftp/src/sftpserver.rs | 15 +++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 9c5928df..c8a58487 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,7 +1,10 @@ use core::marker::PhantomData; -use sshwire::{BinString, TextString, SSHEncode, SSHDecode, SSHSource, SSHSink, WireResult, WireError}; -use sunset::{error, Result}; +use sshwire::{ + BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, + WireResult, +}; +use sunset::{Result, error}; // TODO is utf8 enough, or does this need to be an opaque binstring? #[derive(Debug)] diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 6623b989..0ce22383 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,4 +1,4 @@ -use proto::{StatusCode, Attrs}; +use proto::{Attrs, StatusCode}; pub type Result = core::result::Result; @@ -10,12 +10,17 @@ trait SftpServer { type Handle; // TODO flags struct - async fn open(filename: &str, flags: u32, attrs: &Attrs) -> Result; + async fn open(filename: &str, flags: u32, attrs: &Attrs) + -> Result; /// Close either a file or directory handle async fn close(handle: &Self::Handle) -> Result<()>; - async fn read(handle: &Self::Handle, offset: u64, reply: &mut ReadReply) -> Result<()>; + async fn read( + handle: &Self::Handle, + offset: u64, + reply: &mut ReadReply, + ) -> Result<()>; async fn write(handle: &Self::Handle, offset: u64, buf: &[u8]) -> Result<()>; @@ -29,7 +34,5 @@ pub struct ReadReply<'g, 'a> { } impl<'g, 'a> ReadReply<'g, 'a> { - pub async fn reply(self, data: &[u8]) { - - } + pub async fn reply(self, data: &[u8]) {} } From ddd91e46b6ef0e67a6e610cbaf98f198a3b1a846 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:01:02 +1000 Subject: [PATCH 003/393] sunset-sftp added to Cargo.lock This was an automatic resolution --- Cargo.lock | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 86712c2a..80509a49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2876,6 +2876,14 @@ dependencies = [ "sunset-sshwire-derive", ] +[[package]] +name = "sunset-sftp" +version = "0.1.0" +dependencies = [ + "sunset", + "sunset-sshwire-derive", +] + [[package]] name = "sunset-sshwire-derive" version = "0.2.0" From 146abef2148071f065a338f53365f4b9440ed957 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:01:49 +1000 Subject: [PATCH 004/393] error fix on casting SftpNum to u8 Fixing error message `casting `&SftpNum` as `u8` is invalid: needs casting through a raw pointer firstrust-analyzerE0606` --- sftp/src/proto.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index c8a58487..aab623ab 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -150,12 +150,12 @@ pub enum SftpNum { impl SftpNum { fn is_request(&self) -> bool { // TODO SSH_FXP_EXTENDED - (2..=99).contains(self as u8) + (2..=99).contains(*self as u8) } fn is_response(&self) -> bool { // TODO SSH_FXP_EXTENDED_REPLY - (100..=199).contains(self as u8) + (100..=199).contains(*self as u8) } } From 7028e03cdb0dafdb3becd67bc4a96ad0f22a3a7d Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:18:42 +1000 Subject: [PATCH 005/393] fixing error in sftpmessages macro_rules SSHDecode implementation I believe that $SSH_MESSAGE_NAME should be $SSH_FXP_NAME for this macro to work and that it was intended for this match ty expansion. --- sftp/src/proto.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index aab623ab..f463ad49 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -208,8 +208,11 @@ impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { // eg // MessageNumber::SSH_MSG_KEXINIT => Packet::KexInit( // ... + // Error: MessageNumber::$SSH_MESSAGE_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), + // ^^^^^^^^^^^^^^^^^ expected identifier + // Changed SSH_MESSAGE_NAME to SSH_FXP_NAME. I believe that this is the intended thing to do $( - MessageNumber::$SSH_MESSAGE_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), + MessageNumber::$SSH_FXP_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), )* }; Ok(p) From 6e2c6b8a927d912fb32947b9793bfd7d059c306f Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:20:49 +1000 Subject: [PATCH 006/393] fixing use of sftp proto --- sftp/src/sftpserver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 0ce22383..7b119849 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,4 +1,4 @@ -use proto::{Attrs, StatusCode}; +use crate::proto::{Attrs, StatusCode}; pub type Result = core::result::Result; From 0e15a7f8995b72daad94cdbb9c33ac53e452220d Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:34:46 +1000 Subject: [PATCH 007/393] Adding some extra uses in proto.rs --- sftp/src/proto.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index f463ad49..c3aa680c 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,11 +1,15 @@ use core::marker::PhantomData; -use sshwire::{ +use sunset::packets::{MessageNumber, Packet}; +use sunset::sshwire::{ BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, WireResult, }; use sunset::{Result, error}; +use sunset_sshwire_derive::{SSHDecode, SSHEncode}; +use sunset::{Result, error}; + // TODO is utf8 enough, or does this need to be an opaque binstring? #[derive(Debug)] pub struct Filename<'a>(TextString<'a>); From 2fd93724657557112d2d37eb097dded4312e76ea Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:37:01 +1000 Subject: [PATCH 008/393] making proto StatusCode pub --- sftp/src/proto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index c3aa680c..a016d9c2 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -93,7 +93,7 @@ pub struct ReqId(pub u32); #[derive(Debug, SSHEncode, SSHDecode)] #[repr(u8)] #[allow(non_camel_case_types)] -enum StatusCode { +pub enum StatusCode { SSH_FX_OK = 0, SSH_FX_EOF = 1, SSH_FX_NO_SUCH_FILE = 2, From 4cd832f0616817157f20190060cf841747b838c7 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:38:27 +1000 Subject: [PATCH 009/393] Fixing sftpmessages SftpNum impl issue providing the correct data type to contains (a reference to a u8) --- sftp/src/proto.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index a016d9c2..9bb23a97 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -154,12 +154,12 @@ pub enum SftpNum { impl SftpNum { fn is_request(&self) -> bool { // TODO SSH_FXP_EXTENDED - (2..=99).contains(*self as u8) + (2..=99).contains(&(*self as u8)) } fn is_response(&self) -> bool { // TODO SSH_FXP_EXTENDED_REPLY - (100..=199).contains(*self as u8) + (100..=199).contains(&(*self as u8)) } } From 9a582edde3e7047ac760e94b69b0bb58b8d5a6d3 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:39:17 +1000 Subject: [PATCH 010/393] removing reduntant use in proto.rs --- sftp/src/proto.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 9bb23a97..5fe74f2d 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -8,7 +8,6 @@ use sunset::sshwire::{ use sunset::{Result, error}; use sunset_sshwire_derive::{SSHDecode, SSHEncode}; -use sunset::{Result, error}; // TODO is utf8 enough, or does this need to be an opaque binstring? #[derive(Debug)] From f0edceafded08b4c40b71a6e8eea684e28e8df30 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:47:42 +1000 Subject: [PATCH 011/393] Added variant to proto StatusCode --- sftp/src/proto.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 5fe74f2d..81b8710f 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -93,15 +93,25 @@ pub struct ReqId(pub u32); #[repr(u8)] #[allow(non_camel_case_types)] pub enum StatusCode { + #[sshwire(variant = "ssh_fx_ok")] SSH_FX_OK = 0, + #[sshwire(variant = "ssh_fx_eof")] SSH_FX_EOF = 1, + #[sshwire(variant = "ssh_fx_no_such_file")] SSH_FX_NO_SUCH_FILE = 2, + #[sshwire(variant = "ssh_fx_premission_denied")] SSH_FX_PERMISSION_DENIED = 3, + #[sshwire(variant = "ssh_fx_failure")] SSH_FX_FAILURE = 4, + #[sshwire(variant = "ssh_fx_bad_message")] SSH_FX_BAD_MESSAGE = 5, + #[sshwire(variant = "ssh_fx_no_connection")] SSH_FX_NO_CONNECTION = 6, + #[sshwire(variant = "ssh_fx_connection_lost")] SSH_FX_CONNECTION_LOST = 7, + #[sshwire(variant = "ssh_fx_unsupported")] SSH_FX_OP_UNSUPPORTED = 8, + #[sshwire(variant = "ssh_fx_unsupported")] Other(u8), } From 5fcef05fda4d2c1d63a0bc3ca0cfae0147ef17a5 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:50:23 +1000 Subject: [PATCH 012/393] cleaning redundant comments the commit 7028e03cdb0dafdb3becd67bc4a96ad0f22a3a7d covers this --- sftp/src/proto.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 81b8710f..dacc3513 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -221,9 +221,6 @@ impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { // eg // MessageNumber::SSH_MSG_KEXINIT => Packet::KexInit( // ... - // Error: MessageNumber::$SSH_MESSAGE_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), - // ^^^^^^^^^^^^^^^^^ expected identifier - // Changed SSH_MESSAGE_NAME to SSH_FXP_NAME. I believe that this is the intended thing to do $( MessageNumber::$SSH_FXP_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), )* From eb93b674f10988a2b37786b6bf9ea8ebd4142c9c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 13:00:31 +1000 Subject: [PATCH 013/393] Removing some unused lifetimes that caused issues They can be added when needed --- sftp/src/proto.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index dacc3513..9e33482f 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -17,18 +17,17 @@ pub struct Filename<'a>(TextString<'a>); struct FileHandle<'a>(pub BinString<'a>); #[derive(Debug, SSHEncode, SSHDecode)] -pub struct InitVersion<'a> { +pub struct InitVersion { // No ReqId for SSH_FXP_INIT pub version: u32, // TODO variable number of ExtPair - pub _ext: &'a PhantomData<()>, } #[derive(Debug, SSHEncode, SSHDecode)] pub struct Open<'a> { pub filename: Filename<'a>, pub pflags: u32, - pub attrs: Attrs<'a>, + pub attrs: Attrs, } #[derive(Debug, SSHEncode, SSHDecode)] @@ -75,6 +74,7 @@ pub struct Data<'a> { pub struct Name<'a> { pub count: u32, // TODO repeat NameEntry + _pd: &'a PhantomData<()>, } #[derive(Debug, SSHEncode, SSHDecode)] @@ -83,7 +83,7 @@ pub struct NameEntry<'a> { /// longname is an undefined text line like "ls -l", /// SHOULD NOT be used. pub _longname: Filename<'a>, - pub attrs: Attrs<'a>, + pub attrs: Attrs, } #[derive(Debug, SSHEncode, SSHDecode, Clone, Copy)] @@ -122,7 +122,7 @@ pub struct ExtPair<'a> { } #[derive(Debug)] -pub struct Attrs<'a> { +pub struct Attrs { // flags: u32, defines used attributes pub size: Option, pub uid: Option, @@ -327,8 +327,8 @@ sftpmessages![ // Message number ranges are also used by Sftpnum::is_request and is_response. -(1, Init, InitVersion<'a>, SSH_FXP_INIT), -(2, Version, InitVersion<'a>, SSH_FXP_VERSION), +(1, Init, InitVersion, SSH_FXP_INIT), +(2, Version, InitVersion, SSH_FXP_VERSION), // Requests (3, Open, Open<'a>, SSH_FXP_OPEN), From 6b4f47131e7ad57b2b0cae180d4a456df07f4a7c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 13:06:22 +1000 Subject: [PATCH 014/393] Chasing SSHEncode,SSHDecode issues Adding the derive for Filename and Attrs --- sftp/src/proto.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 9e33482f..76f1dfde 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -10,7 +10,7 @@ use sunset::{Result, error}; use sunset_sshwire_derive::{SSHDecode, SSHEncode}; // TODO is utf8 enough, or does this need to be an opaque binstring? -#[derive(Debug)] +#[derive(Debug, SSHEncode, SSHDecode)] pub struct Filename<'a>(TextString<'a>); #[derive(Debug, SSHEncode, SSHDecode)] @@ -121,7 +121,7 @@ pub struct ExtPair<'a> { pub data: BinString<'a>, } -#[derive(Debug)] +#[derive(Debug, SSHEncode, SSHDecode)] pub struct Attrs { // flags: u32, defines used attributes pub size: Option, From 80288adad1c3c4de3788a3259ec1a8de69403396 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 13:56:58 +1000 Subject: [PATCH 015/393] Adding empty vanilla DirReply struct as ReadReply --- sftp/src/sftpserver.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 7b119849..0b249bad 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -36,3 +36,11 @@ pub struct ReadReply<'g, 'a> { impl<'g, 'a> ReadReply<'g, 'a> { pub async fn reply(self, data: &[u8]) {} } + +pub struct DirReply<'g, 'a> { + chan: ChanOut<'g, 'a>, +} + +impl<'g, 'a> DirReply<'g, 'a> { + pub async fn reply(self, data: &[u8]) {} +} From 49d625c8f05e7850b15a8fed6e619a834c0a3a53 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 14:02:56 +1000 Subject: [PATCH 016/393] Adding Vanilla ChanOut to finish the placeholders for Dir and Read Reply --- sftp/src/sftpserver.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 0b249bad..8a6ce16f 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,4 +1,5 @@ use crate::proto::{Attrs, StatusCode}; +use core::marker::PhantomData; pub type Result = core::result::Result; @@ -44,3 +45,9 @@ pub struct DirReply<'g, 'a> { impl<'g, 'a> DirReply<'g, 'a> { pub async fn reply(self, data: &[u8]) {} } + +// TODO: Implement correct Channel Out +pub struct ChanOut<'g, 'a> { + _phantom_g: PhantomData<&'g ()>, + _phantom_a: PhantomData<&'a ()>, +} From 72c4b97c5abefa568f490f592c36a39e2c9c4df0 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 14:06:27 +1000 Subject: [PATCH 017/393] Extra fix for SftpNum to avoid moving issues --- sftp/src/proto.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 76f1dfde..9bad04b4 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -149,7 +149,7 @@ macro_rules! sftpmessages { ) => { -#[derive(Debug)] +#[derive(Debug, Clone)] #[repr(u8)] #[allow(non_camel_case_types)] pub enum SftpNum { @@ -163,12 +163,12 @@ pub enum SftpNum { impl SftpNum { fn is_request(&self) -> bool { // TODO SSH_FXP_EXTENDED - (2..=99).contains(&(*self as u8)) + (2..=99).contains(&(self.clone() as u8)) } fn is_response(&self) -> bool { // TODO SSH_FXP_EXTENDED_REPLY - (100..=199).contains(&(*self as u8)) + (100..=199).contains(&(self.clone() as u8)) } } From 0a822c05f50fbebf3cf5b7c9092e8070d1aba65e Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 15:11:50 +1000 Subject: [PATCH 018/393] WIP: Replacing sunset::Result with proto::Result Fixing issues with error handling is far from finished sunset::Result cannot take the new Errors definded in proto.rs therefore I have added error conversion for sftp errors to Sunset global error. --- sftp/src/proto.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 9bad04b4..c6c43336 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,11 +1,12 @@ use core::marker::PhantomData; +use sunset::error; +use sunset::error::Error as SunsetError; use sunset::packets::{MessageNumber, Packet}; use sunset::sshwire::{ BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, WireResult, }; -use sunset::{Result, error}; use sunset_sshwire_derive::{SSHDecode, SSHEncode}; @@ -134,10 +135,25 @@ pub struct Attrs { // TODO extensions } -enum Error { +#[derive(Debug)] +pub enum Error { UnknownPacket { number: u8 }, } +pub type Result = core::result::Result; + +impl From for SunsetError { + fn from(error: Error) -> SunsetError { + SunsetError::Custom { + msg: match error { + Error::UnknownPacket { number } => { + format_args!("Unknown SFTP packet: {}", number) + } + }, + } + } +} + macro_rules! sftpmessages { ( $( ( $message_num:literal, From bdc24fe1350a631df8b4512b2855cd01bddbb318 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 11:07:58 +1000 Subject: [PATCH 019/393] commenting out sftpmessages macro rules to focus on the foundational SFTP data types --- sftp/src/proto.rs | 407 +++++++++++++++++++++++----------------------- 1 file changed, 203 insertions(+), 204 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index c6c43336..2fee115a 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -154,207 +154,206 @@ impl From for SunsetError { } } -macro_rules! sftpmessages { - ( - $( ( $message_num:literal, - $SpecificPacketVariant:ident, - $SpecificPacketType:ty, - $SSH_FXP_NAME:ident - ), - )* - ) => { - - -#[derive(Debug, Clone)] -#[repr(u8)] -#[allow(non_camel_case_types)] -pub enum SftpNum { - // variants are eg - // SSH_FXP_OPEN = 3, - $( - $SSH_FXP_NAME = $message_num, - )* -} - -impl SftpNum { - fn is_request(&self) -> bool { - // TODO SSH_FXP_EXTENDED - (2..=99).contains(&(self.clone() as u8)) - } - - fn is_response(&self) -> bool { - // TODO SSH_FXP_EXTENDED_REPLY - (100..=199).contains(&(self.clone() as u8)) - } -} - -impl TryFrom for SftpNum { - type Error = Error; - fn try_from(v: u8) -> Result { - match v { - // eg - // 3 => Ok(SftpNum::SSH_FXP_OPEN) - $( - $message_num => Ok(SftpNum::$SSH_FXP_NAME), - )* - _ => { - Err(Error::UnknownPacket { number: v }) - } - } - } -} - -impl SSHEncode for SftpPacket<'_> { - fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { - let t = self.message_num() as u8; - t.enc(s)?; - match self { - // eg - // Packet::KexInit(p) => { - // ... - $( - Packet::$SpecificPacketVariant(p) => { - p.enc(s)? - } - )* - }; - Ok(()) - } -} - -impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { - fn dec(s: &mut S) -> WireResult - where S: SSHSource<'de> { - let msg_num = u8::dec(s)?; - let ty = MessageNumber::try_from(msg_num); - let ty = match ty { - Ok(t) => t, - Err(_) => return Err(WireError::UnknownPacket { number: msg_num }) - }; - - // Decode based on the message number - let p = match ty { - // eg - // MessageNumber::SSH_MSG_KEXINIT => Packet::KexInit( - // ... - $( - MessageNumber::$SSH_FXP_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), - )* - }; - Ok(p) - } -} - -/// Top level SSH packet enum -#[derive(Debug)] -pub enum SftpPacket<'a> { - // eg Open(Open<'a>), - $( - $SpecificPacketVariant($SpecificPacketType), - )* -} - -impl<'a> SftpPacket<'a> { - pub fn sftp_num(&self) -> SftpNum { - match self { - // eg - // SftpPacket::Open(_) => { - // .. - $( - SftpPacket::$SpecificPacketVariant(_) => { - MessageNumber::$SSH_FXP_NAME - } - )* - } - } - - /// Encode a request. - /// - /// Used by a SFTP client. Does not include the length field. - pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> { - if !self.sftp_num().is_request() { - return Err(Error::bug()) - } - - // packet type - self.sftp_num().enc(s)?; - // request ID - id.0.enc(s)?; - // contents - self.enc(s) - } - - /// Decode a response. - /// - /// Used by a SFTP client. Does not include the length field. - pub fn decode_response(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> { - let num = SftpNum::try_from(u8::dec(s)?)?; - - if !num.is_response() { - return error::SSHProto.fail(); - } - - let id = ReqId(u32::dec(s)?); - Ok((id, Self::dec(s))) - } - - /// Decode a request. - /// - /// Used by a SFTP server. Does not include the length field. - pub fn decode_request(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> { - let num = SftpNum::try_from(u8::dec(s)?)?; - - if !num.is_request() { - return error::SSHProto.fail(); - } - - let id = ReqId(u32::dec(s)?); - Ok((id, Self::dec(s))) - } - - /// Encode a response. - /// - /// Used by a SFTP server. Does not include the length field. - pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> { - if !self.sftp_num().is_response() { - return Err(Error::bug()) - } - - // packet type - self.sftp_num().enc(s)?; - // request ID - id.0.enc(s)?; - // contents - self.enc(s) - } -} - -$( -impl<'a> From<$SpecificPacketType> for SftpPacket<'a> { - fn from(s: $SpecificPacketType) -> SftpPacket<'a> { - SftpPacket::$SpecificPacketVariant(s) - } -} -)* - -} } // macro - -sftpmessages![ - -// Message number ranges are also used by Sftpnum::is_request and is_response. - -(1, Init, InitVersion, SSH_FXP_INIT), -(2, Version, InitVersion, SSH_FXP_VERSION), - -// Requests -(3, Open, Open<'a>, SSH_FXP_OPEN), -(4, Close, Close<'a>, SSH_FXP_CLOSE), -(5, Read, Read<'a>, SSH_FXP_READ), - -// Responses -(101, Status, Status<'a>, SSH_FXP_STATUS), -(102, Handle, Handle<'a>, SSH_FXP_HANDLE), -(103, Data, Data<'a>, SSH_FXP_DATA), -(104, Name, Name<'a>, SSH_FXP_NAME), - -]; +// macro_rules! sftpmessages { +// ( +// $( ( $message_num:literal, +// $SpecificPacketVariant:ident, +// $SpecificPacketType:ty, +// $SSH_FXP_NAME:ident +// ), +// )* +// ) => { + +// #[derive(Debug, Clone)] +// #[repr(u8)] +// #[allow(non_camel_case_types)] +// pub enum SftpNum { +// // variants are eg +// // SSH_FXP_OPEN = 3, +// $( +// $SSH_FXP_NAME = $message_num, +// )* +// } + +// impl SftpNum { +// fn is_request(&self) -> bool { +// // TODO SSH_FXP_EXTENDED +// (2..=99).contains(&(self.clone() as u8)) +// } + +// fn is_response(&self) -> bool { +// // TODO SSH_FXP_EXTENDED_REPLY +// (100..=199).contains(&(self.clone() as u8)) +// } +// } + +// impl TryFrom for SftpNum { +// type Error = Error; +// fn try_from(v: u8) -> Result { +// match v { +// // eg +// // 3 => Ok(SftpNum::SSH_FXP_OPEN) +// $( +// $message_num => Ok(SftpNum::$SSH_FXP_NAME), +// )* +// _ => { +// Err(Error::UnknownPacket { number: v }) +// } +// } +// } +// } + +// impl SSHEncode for SftpPacket<'_> { +// fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { +// let t = self.message_num() as u8; +// t.enc(s)?; +// match self { +// // eg +// // Packet::KexInit(p) => { +// // ... +// $( +// Packet::$SpecificPacketVariant(p) => { +// p.enc(s)? +// } +// )* +// }; +// Ok(()) +// } +// } + +// impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { +// fn dec(s: &mut S) -> WireResult +// where S: SSHSource<'de> { +// let msg_num = u8::dec(s)?; +// let ty = MessageNumber::try_from(msg_num); +// let ty = match ty { +// Ok(t) => t, +// Err(_) => return Err(WireError::UnknownPacket { number: msg_num }) +// }; + +// // Decode based on the message number +// let p = match ty { +// // eg +// // MessageNumber::SSH_MSG_KEXINIT => Packet::KexInit( +// // ... +// $( +// MessageNumber::$SSH_FXP_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), +// )* +// }; +// Ok(p) +// } +// } + +// /// Top level SSH packet enum +// #[derive(Debug)] +// pub enum SftpPacket<'a> { +// // eg Open(Open<'a>), +// $( +// $SpecificPacketVariant($SpecificPacketType), +// )* +// } + +// impl<'a> SftpPacket<'a> { +// pub fn sftp_num(&self) -> SftpNum { +// match self { +// // eg +// // SftpPacket::Open(_) => { +// // .. +// $( +// SftpPacket::$SpecificPacketVariant(_) => { +// MessageNumber::$SSH_FXP_NAME +// } +// )* +// } +// } + +// /// Encode a request. +// /// +// /// Used by a SFTP client. Does not include the length field. +// pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> { +// if !self.sftp_num().is_request() { +// return Err(Error::bug()) +// } + +// // packet type +// self.sftp_num().enc(s)?; +// // request ID +// id.0.enc(s)?; +// // contents +// self.enc(s) +// } + +// /// Decode a response. +// /// +// /// Used by a SFTP client. Does not include the length field. +// pub fn decode_response(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> { +// let num = SftpNum::try_from(u8::dec(s)?)?; + +// if !num.is_response() { +// return error::SSHProto.fail(); +// } + +// let id = ReqId(u32::dec(s)?); +// Ok((id, Self::dec(s))) +// } + +// /// Decode a request. +// /// +// /// Used by a SFTP server. Does not include the length field. +// pub fn decode_request(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> { +// let num = SftpNum::try_from(u8::dec(s)?)?; + +// if !num.is_request() { +// return error::SSHProto.fail(); +// } + +// let id = ReqId(u32::dec(s)?); +// Ok((id, Self::dec(s))) +// } + +// /// Encode a response. +// /// +// /// Used by a SFTP server. Does not include the length field. +// pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> { +// if !self.sftp_num().is_response() { +// return Err(Error::bug()) +// } + +// // packet type +// self.sftp_num().enc(s)?; +// // request ID +// id.0.enc(s)?; +// // contents +// self.enc(s) +// } +// } + +// $( +// impl<'a> From<$SpecificPacketType> for SftpPacket<'a> { +// fn from(s: $SpecificPacketType) -> SftpPacket<'a> { +// SftpPacket::$SpecificPacketVariant(s) +// } +// } +// )* + +// } } // macro + +// sftpmessages![ + +// // Message number ranges are also used by Sftpnum::is_request and is_response. + +// (1, Init, InitVersion, SSH_FXP_INIT), +// (2, Version, InitVersion, SSH_FXP_VERSION), + +// // Requests +// (3, Open, Open<'a>, SSH_FXP_OPEN), +// (4, Close, Close<'a>, SSH_FXP_CLOSE), +// (5, Read, Read<'a>, SSH_FXP_READ), + +// // Responses +// (101, Status, Status<'a>, SSH_FXP_STATUS), +// (102, Handle, Handle<'a>, SSH_FXP_HANDLE), +// (103, Data, Data<'a>, SSH_FXP_DATA), +// (104, Name, Name<'a>, SSH_FXP_NAME), + +// ]; From 7e2f02b0e03f035661594ddee9a3c08427f863c0 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 11:11:34 +1000 Subject: [PATCH 020/393] commenting out Result and ane extra SunsetError implementation too --- sftp/src/proto.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 2fee115a..10d9ad2e 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -140,19 +140,19 @@ pub enum Error { UnknownPacket { number: u8 }, } -pub type Result = core::result::Result; - -impl From for SunsetError { - fn from(error: Error) -> SunsetError { - SunsetError::Custom { - msg: match error { - Error::UnknownPacket { number } => { - format_args!("Unknown SFTP packet: {}", number) - } - }, - } - } -} +// pub type Result = core::result::Result; + +// impl From for SunsetError { +// fn from(error: Error) -> SunsetError { +// SunsetError::Custom { +// msg: match error { +// Error::UnknownPacket { number } => { +// format_args!("Unknown SFTP packet: {}", number) +// } +// }, +// } +// } +// } // macro_rules! sftpmessages { // ( From 0001130c2e5114e64db80918dfb5dd2b32e8a047 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 11:54:28 +1000 Subject: [PATCH 021/393] Needed to add variands for SSHEncode/Decode for u64 --- src/sshwire.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/sshwire.rs b/src/sshwire.rs index bfd698ab..1733ee5d 100644 --- a/src/sshwire.rs +++ b/src/sshwire.rs @@ -486,6 +486,12 @@ impl SSHEncode for u32 { } } +impl SSHEncode for u64 { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + s.push(&self.to_be_bytes()) + } +} + // no length prefix impl SSHEncode for &[u8] { fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { @@ -555,6 +561,16 @@ impl<'de> SSHDecode<'de> for u32 { } } +impl<'de> SSHDecode<'de> for u64 { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + let t = s.take(core::mem::size_of::())?; + Ok(u64::from_be_bytes(t.try_into().unwrap())) + } +} + /// Decodes a SSH name string. Must be ASCII /// without control characters. RFC4251 section 6. pub fn try_as_ascii(t: &[u8]) -> WireResult<&AsciiStr> { From 4491b7ee818ca2e7b6876773efefbd2ef9bc9f05 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 11:55:33 +1000 Subject: [PATCH 022/393] Removing phantomData for the moment to avoid SSHEncode/Decode issues --- sftp/src/proto.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 10d9ad2e..25722a3b 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,5 +1,3 @@ -use core::marker::PhantomData; - use sunset::error; use sunset::error::Error as SunsetError; use sunset::packets::{MessageNumber, Packet}; @@ -52,12 +50,12 @@ pub struct Write<'a> { // Responses -#[derive(Debug, SSHEncode, SSHDecode)] -pub struct Status<'a> { - pub code: StatusCode, - pub message: TextString<'a>, - pub lang: TextString<'a>, -} +// #[derive(Debug, SSHEncode, SSHDecode)] +// pub struct Status<'a> { +// pub code: StatusCode, +// pub message: TextString<'a>, +// pub lang: TextString<'a>, +// } #[derive(Debug, SSHEncode, SSHDecode)] pub struct Handle<'a> { @@ -72,10 +70,9 @@ pub struct Data<'a> { } #[derive(Debug, SSHEncode, SSHDecode)] -pub struct Name<'a> { +pub struct Name { pub count: u32, // TODO repeat NameEntry - _pd: &'a PhantomData<()>, } #[derive(Debug, SSHEncode, SSHDecode)] From 33dd0d724d9f22e3c5fa54b6116e5c6d6733881e Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 11:58:19 +1000 Subject: [PATCH 023/393] Implementing SSHEncode/Decode for Attrs To do so, I introduced the method flags(), and the enum AttrsFlags to serialise and deserialise the flags --- sftp/src/proto.rs | 116 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 4 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 25722a3b..7f7aeef7 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -119,7 +119,7 @@ pub struct ExtPair<'a> { pub data: BinString<'a>, } -#[derive(Debug, SSHEncode, SSHDecode)] +#[derive(Debug, Default)] pub struct Attrs { // flags: u32, defines used attributes pub size: Option, @@ -132,11 +132,119 @@ pub struct Attrs { // TODO extensions } -#[derive(Debug)] -pub enum Error { - UnknownPacket { number: u8 }, +#[repr(u32)] +#[allow(non_camel_case_types)] +pub enum AttrsFlags { + SSH_FILEXFER_ATTR_SIZE = 0x01, + SSH_FILEXFER_ATTR_UIDGID = 0x02, + SSH_FILEXFER_ATTR_PERMISSIONS = 0x04, + SSH_FILEXFER_ATTR_ACMODTIME = 0x08, + SSH_FILEXFER_ATTR_EXTENDED = 0x80000000, +} +impl core::ops::AddAssign for u32 { + fn add_assign(&mut self, other: AttrsFlags) { + *self |= other as u32; + } +} + +impl core::ops::BitAnd for u32 { + type Output = u32; + + fn bitand(self, rhs: AttrsFlags) -> Self::Output { + self & rhs as u32 + } +} + +impl Attrs { + pub fn flags(&self) -> u32 { + let mut flags: u32 = 0; + if self.size.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_SIZE + } + if self.uid.is_some() || self.gid.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_UIDGID + } + if self.permissions.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_PERMISSIONS + } + if self.atime.is_some() || self.mtime.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_ACMODTIME + } + // TODO: Implement extensions + // if self.ext_count.is_some() { + // flags += AttrsFlags::SSH_FILEXFER_ATTR_EXTENDED + // } + + flags + } } +impl SSHEncode for Attrs { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + self.flags().enc(s)?; + + // IMPORTANT: Order matters in the encoding/decoding since it will be interpreted together with the flags + if let Some(value) = self.size.as_ref() { + value.enc(s)? + } + if let Some(value) = self.uid.as_ref() { + value.enc(s)? + } + if let Some(value) = self.gid.as_ref() { + value.enc(s)? + } + if let Some(value) = self.permissions.as_ref() { + value.enc(s)? + } + if let Some(value) = self.atime.as_ref() { + value.enc(s)? + } + if let Some(value) = self.mtime.as_ref() { + value.enc(s)? + } + // TODO: Implement extensions + // if let Some(value) = self.ext_count.as_ref() { value.enc(s)? } + + Ok(()) + } +} + +impl<'de> SSHDecode<'de> for Attrs { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + let mut attrs = Attrs::default(); + let flags = u32::dec(s)? as u32; + if flags & AttrsFlags::SSH_FILEXFER_ATTR_SIZE != 0 { + attrs.size = Some(u64::dec(s)?); + } + if flags & AttrsFlags::SSH_FILEXFER_ATTR_UIDGID != 0 { + attrs.uid = Some(u32::dec(s)?); + attrs.gid = Some(u32::dec(s)?); + } + if flags & AttrsFlags::SSH_FILEXFER_ATTR_PERMISSIONS != 0 { + attrs.permissions = Some(u32::dec(s)?); + } + if flags & AttrsFlags::SSH_FILEXFER_ATTR_ACMODTIME != 0 { + attrs.atime = Some(u32::dec(s)?); + attrs.mtime = Some(u32::dec(s)?); + } + // TODO: Implement extensions + // if flags & AttrsFlags::SSH_FILEXFER_ATTR_EXTENDED != 0{ + + // todo!("Not implemented"); + // } + + Ok(attrs) + } +} + +// #[derive(Debug)] +// pub enum Error { +// UnknownPacket { number: u8 }, +// } + // pub type Result = core::result::Result; // impl From for SunsetError { From 4b8cc09012d332dcb054bcc5f1f5106fd286a468 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 11:59:43 +1000 Subject: [PATCH 024/393] Added a response variant "ResponseAttributes" to complete all server responses SFTP packets Feel free to rename it --- sftp/src/proto.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 7f7aeef7..d6fb0af4 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -84,6 +84,13 @@ pub struct NameEntry<'a> { pub attrs: Attrs, } +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct ResponseAttributes { + pub attrs: Attrs, +} + +// Requests/Responses data types + #[derive(Debug, SSHEncode, SSHDecode, Clone, Copy)] pub struct ReqId(pub u32); From 8b9097bb32b22cf3f7bfda76f5ebb74968ca55fd Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 12:06:04 +1000 Subject: [PATCH 025/393] Fixed typo and change other variant string to ssh_fx_other --- sftp/src/proto.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index d6fb0af4..0e5e6484 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -104,7 +104,7 @@ pub enum StatusCode { SSH_FX_EOF = 1, #[sshwire(variant = "ssh_fx_no_such_file")] SSH_FX_NO_SUCH_FILE = 2, - #[sshwire(variant = "ssh_fx_premission_denied")] + #[sshwire(variant = "ssh_fx_permission_denied")] SSH_FX_PERMISSION_DENIED = 3, #[sshwire(variant = "ssh_fx_failure")] SSH_FX_FAILURE = 4, @@ -116,7 +116,7 @@ pub enum StatusCode { SSH_FX_CONNECTION_LOST = 7, #[sshwire(variant = "ssh_fx_unsupported")] SSH_FX_OP_UNSUPPORTED = 8, - #[sshwire(variant = "ssh_fx_unsupported")] + #[sshwire(variant = "ssh_fx_other")] Other(u8), } From 00de3a03bd5158f4abfae3807473084e799aa7f6 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 15:34:36 +1000 Subject: [PATCH 026/393] isolating proto.rs issues. Commenting out sftpserver --- sftp/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 4c2f3ffe..0368d938 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,2 +1,2 @@ mod proto; -mod sftpserver; +// mod sftpserver; From 85a11e057331a93c97db38c8f8bae5233c7e11c2 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 15:40:28 +1000 Subject: [PATCH 027/393] Modifying packets.rs ParseContext for allowing proto.rs StatusCode to use #[sshwire(unknown)] Without changing some visiblitities, the code generated by SSHDecode (SSHDecodeEnum) for proto.rs would try accessing s.ctx().seen_unknown and Unknown::new() throwing errors. Am I doing using SSHDecode wrong? --- src/packets.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index fa55c812..28e48c00 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -823,7 +823,7 @@ pub struct DirectTcpip<'a> { pub struct Unknown<'a>(pub &'a [u8]); impl<'a> Unknown<'a> { - fn new(u: &'a [u8]) -> Self { + pub fn new(u: &'a [u8]) -> Self { let u = Unknown(u); trace!("saw unknown variant \"{u}\""); u @@ -864,7 +864,7 @@ pub struct ParseContext { // Set to true if an unknown variant is encountered. // Packet length checks should be omitted in that case. - pub(crate) seen_unknown: bool, + pub seen_unknown: bool, } impl ParseContext { From 3ef03aa6fb831f720bb1d8805f30e4cd9b821ea5 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 15:46:16 +1000 Subject: [PATCH 028/393] proto.rs StatusCode SSHEncode expanded version seems incorrect. I made a StatusCode version with lifetime (to be used by Other) and StatusCode SSHEncode autogenerated enc() function (L584) looks wrong since it is not performing any ::sunset::sshwire::SSHEncode::enc() Am I using SSHEncode wrong for this enum? --- sftp/out/cargo-expand-sftp.rs | 829 ++++++++++++++++++++++++++++++++++ sftp/src/proto.rs | 9 +- 2 files changed, 834 insertions(+), 4 deletions(-) create mode 100644 sftp/out/cargo-expand-sftp.rs diff --git a/sftp/out/cargo-expand-sftp.rs b/sftp/out/cargo-expand-sftp.rs new file mode 100644 index 00000000..b2766361 --- /dev/null +++ b/sftp/out/cargo-expand-sftp.rs @@ -0,0 +1,829 @@ +#![feature(prelude_import)] +#[macro_use] +extern crate std; +#[prelude_import] +use std::prelude::rust_2024::*; +mod proto { + use sunset::error; + use sunset::error::Error as SunsetError; + use sunset::packets::{MessageNumber, Packet, Unknown}; + use sunset::sshwire::{ + BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, + WireResult, + }; + use sunset_sshwire_derive::{SSHDecode, SSHEncode}; + pub struct Filename<'a>(TextString<'a>); + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Filename<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Filename", &&self.0) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Filename<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.0, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Filename<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) + } + } + struct FileHandle<'a>(pub BinString<'a>); + #[automatically_derived] + impl<'a> ::core::fmt::Debug for FileHandle<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_tuple_field1_finish(f, "FileHandle", &&self.0) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for FileHandle<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.0, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for FileHandle<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) + } + } + pub struct InitVersion { + pub version: u32, + } + #[automatically_derived] + impl ::core::fmt::Debug for InitVersion { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "InitVersion", + "version", + &&self.version, + ) + } + } + impl ::sunset::sshwire::SSHEncode for InitVersion { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.version, s)?; + Ok(()) + } + } + impl<'de> ::sunset::sshwire::SSHDecode<'de> for InitVersion { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_version = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { version: field_version }) + } + } + pub struct Open<'a> { + pub filename: Filename<'a>, + pub pflags: u32, + pub attrs: Attrs, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Open<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field3_finish( + f, + "Open", + "filename", + &self.filename, + "pflags", + &self.pflags, + "attrs", + &&self.attrs, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Open<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.filename, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.pflags, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.attrs, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Open<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_filename = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_pflags = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_attrs = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { + filename: field_filename, + pflags: field_pflags, + attrs: field_attrs, + }) + } + } + pub struct Close<'a> { + pub handle: FileHandle<'a>, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Close<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "Close", + "handle", + &&self.handle, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Close<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Close<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { handle: field_handle }) + } + } + pub struct Read<'a> { + pub handle: FileHandle<'a>, + pub offset: u64, + pub len: u32, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Read<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field3_finish( + f, + "Read", + "handle", + &self.handle, + "offset", + &self.offset, + "len", + &&self.len, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Read<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.offset, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.len, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Read<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_offset = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_len = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { + handle: field_handle, + offset: field_offset, + len: field_len, + }) + } + } + pub struct Write<'a> { + pub handle: FileHandle<'a>, + pub offset: u64, + pub data: BinString<'a>, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Write<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field3_finish( + f, + "Write", + "handle", + &self.handle, + "offset", + &self.offset, + "data", + &&self.data, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Write<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.offset, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.data, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Write<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_offset = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_data = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { + handle: field_handle, + offset: field_offset, + data: field_data, + }) + } + } + pub struct Handle<'a> { + pub handle: FileHandle<'a>, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Handle<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "Handle", + "handle", + &&self.handle, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Handle<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Handle<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { handle: field_handle }) + } + } + pub struct Data<'a> { + pub handle: FileHandle<'a>, + pub offset: u64, + pub data: BinString<'a>, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Data<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field3_finish( + f, + "Data", + "handle", + &self.handle, + "offset", + &self.offset, + "data", + &&self.data, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Data<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.offset, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.data, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Data<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_offset = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_data = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { + handle: field_handle, + offset: field_offset, + data: field_data, + }) + } + } + pub struct Name { + pub count: u32, + } + #[automatically_derived] + impl ::core::fmt::Debug for Name { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "Name", + "count", + &&self.count, + ) + } + } + impl ::sunset::sshwire::SSHEncode for Name { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.count, s)?; + Ok(()) + } + } + impl<'de> ::sunset::sshwire::SSHDecode<'de> for Name { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_count = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { count: field_count }) + } + } + pub struct NameEntry<'a> { + pub filename: Filename<'a>, + /// longname is an undefined text line like "ls -l", + /// SHOULD NOT be used. + pub _longname: Filename<'a>, + pub attrs: Attrs, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for NameEntry<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field3_finish( + f, + "NameEntry", + "filename", + &self.filename, + "_longname", + &self._longname, + "attrs", + &&self.attrs, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for NameEntry<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.filename, s)?; + ::sunset::sshwire::SSHEncode::enc(&self._longname, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.attrs, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for NameEntry<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_filename = ::sunset::sshwire::SSHDecode::dec(s)?; + let field__longname = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_attrs = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { + filename: field_filename, + _longname: field__longname, + attrs: field_attrs, + }) + } + } + pub struct ResponseAttributes { + pub attrs: Attrs, + } + #[automatically_derived] + impl ::core::fmt::Debug for ResponseAttributes { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "ResponseAttributes", + "attrs", + &&self.attrs, + ) + } + } + impl ::sunset::sshwire::SSHEncode for ResponseAttributes { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.attrs, s)?; + Ok(()) + } + } + impl<'de> ::sunset::sshwire::SSHDecode<'de> for ResponseAttributes { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_attrs = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { attrs: field_attrs }) + } + } + pub struct ReqId(pub u32); + #[automatically_derived] + impl ::core::fmt::Debug for ReqId { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_tuple_field1_finish(f, "ReqId", &&self.0) + } + } + impl ::sunset::sshwire::SSHEncode for ReqId { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.0, s)?; + Ok(()) + } + } + impl<'de> ::sunset::sshwire::SSHDecode<'de> for ReqId { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) + } + } + #[automatically_derived] + impl ::core::clone::Clone for ReqId { + #[inline] + fn clone(&self) -> ReqId { + let _: ::core::clone::AssertParamIsClone; + *self + } + } + #[automatically_derived] + impl ::core::marker::Copy for ReqId {} + #[repr(u8)] + #[allow(non_camel_case_types)] + pub enum StatusCode<'a> { + #[sshwire(variant = "ssh_fx_ok")] + SSH_FX_OK = 0, + #[sshwire(variant = "ssh_fx_eof")] + SSH_FX_EOF = 1, + #[sshwire(variant = "ssh_fx_no_such_file")] + SSH_FX_NO_SUCH_FILE = 2, + #[sshwire(variant = "ssh_fx_permission_denied")] + SSH_FX_PERMISSION_DENIED = 3, + #[sshwire(variant = "ssh_fx_failure")] + SSH_FX_FAILURE = 4, + #[sshwire(variant = "ssh_fx_bad_message")] + SSH_FX_BAD_MESSAGE = 5, + #[sshwire(variant = "ssh_fx_no_connection")] + SSH_FX_NO_CONNECTION = 6, + #[sshwire(variant = "ssh_fx_connection_lost")] + SSH_FX_CONNECTION_LOST = 7, + #[sshwire(variant = "ssh_fx_unsupported")] + SSH_FX_OP_UNSUPPORTED = 8, + #[sshwire(unknown)] + Other(Unknown<'a>), + } + #[automatically_derived] + #[allow(non_camel_case_types)] + impl<'a> ::core::fmt::Debug for StatusCode<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + StatusCode::SSH_FX_OK => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_OK") + } + StatusCode::SSH_FX_EOF => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_EOF") + } + StatusCode::SSH_FX_NO_SUCH_FILE => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_NO_SUCH_FILE") + } + StatusCode::SSH_FX_PERMISSION_DENIED => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_PERMISSION_DENIED") + } + StatusCode::SSH_FX_FAILURE => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_FAILURE") + } + StatusCode::SSH_FX_BAD_MESSAGE => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_BAD_MESSAGE") + } + StatusCode::SSH_FX_NO_CONNECTION => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_NO_CONNECTION") + } + StatusCode::SSH_FX_CONNECTION_LOST => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_CONNECTION_LOST") + } + StatusCode::SSH_FX_OP_UNSUPPORTED => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_OP_UNSUPPORTED") + } + StatusCode::Other(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Other", + &__self_0, + ) + } + } + } + } + impl<'a> ::sunset::sshwire::SSHEncode for StatusCode<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + match *self { + Self::SSH_FX_OK => {} + Self::SSH_FX_EOF => {} + Self::SSH_FX_NO_SUCH_FILE => {} + Self::SSH_FX_PERMISSION_DENIED => {} + Self::SSH_FX_FAILURE => {} + Self::SSH_FX_BAD_MESSAGE => {} + Self::SSH_FX_NO_CONNECTION => {} + Self::SSH_FX_CONNECTION_LOST => {} + Self::SSH_FX_OP_UNSUPPORTED => {} + Self::Other(ref i) => { + return Err(::sunset::sshwire::WireError::UnknownVariant); + } + } + #[allow(unreachable_code)] Ok(()) + } + } + impl<'a> ::sunset::sshwire::SSHEncodeEnum for StatusCode<'a> { + fn variant_name(&self) -> ::sunset::sshwire::WireResult<&'static str> { + let r = match self { + Self::SSH_FX_OK => "ssh_fx_ok", + Self::SSH_FX_EOF => "ssh_fx_eof", + Self::SSH_FX_NO_SUCH_FILE => "ssh_fx_no_such_file", + Self::SSH_FX_PERMISSION_DENIED => "ssh_fx_permission_denied", + Self::SSH_FX_FAILURE => "ssh_fx_failure", + Self::SSH_FX_BAD_MESSAGE => "ssh_fx_bad_message", + Self::SSH_FX_NO_CONNECTION => "ssh_fx_no_connection", + Self::SSH_FX_CONNECTION_LOST => "ssh_fx_connection_lost", + Self::SSH_FX_OP_UNSUPPORTED => "ssh_fx_unsupported", + Self::Other(_) => { + return Err(::sunset::sshwire::WireError::UnknownVariant); + } + }; + #[allow(unreachable_code)] Ok(r) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecodeEnum<'de> for StatusCode<'a> + where + 'de: 'a, + { + fn dec_enum>( + s: &mut S, + variant: &'de [u8], + ) -> ::sunset::sshwire::WireResult { + let var_str = ::sunset::sshwire::try_as_ascii_str(variant).ok(); + let r = match var_str { + Some("ssh_fx_ok") => Self::SSH_FX_OK, + Some("ssh_fx_eof") => Self::SSH_FX_EOF, + Some("ssh_fx_no_such_file") => Self::SSH_FX_NO_SUCH_FILE, + Some("ssh_fx_permission_denied") => Self::SSH_FX_PERMISSION_DENIED, + Some("ssh_fx_failure") => Self::SSH_FX_FAILURE, + Some("ssh_fx_bad_message") => Self::SSH_FX_BAD_MESSAGE, + Some("ssh_fx_no_connection") => Self::SSH_FX_NO_CONNECTION, + Some("ssh_fx_connection_lost") => Self::SSH_FX_CONNECTION_LOST, + Some("ssh_fx_unsupported") => Self::SSH_FX_OP_UNSUPPORTED, + _ => { + s.ctx().seen_unknown = true; + Self::Other(Unknown::new(variant)) + } + }; + Ok(r) + } + } + pub struct ExtPair<'a> { + pub name: &'a str, + pub data: BinString<'a>, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for ExtPair<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field2_finish( + f, + "ExtPair", + "name", + &self.name, + "data", + &&self.data, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for ExtPair<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.name, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.data, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for ExtPair<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_name = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_data = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { + name: field_name, + data: field_data, + }) + } + } + pub struct Attrs { + pub size: Option, + pub uid: Option, + pub gid: Option, + pub permissions: Option, + pub atime: Option, + pub mtime: Option, + pub ext_count: Option, + } + #[automatically_derived] + impl ::core::fmt::Debug for Attrs { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + let names: &'static _ = &[ + "size", + "uid", + "gid", + "permissions", + "atime", + "mtime", + "ext_count", + ]; + let values: &[&dyn ::core::fmt::Debug] = &[ + &self.size, + &self.uid, + &self.gid, + &self.permissions, + &self.atime, + &self.mtime, + &&self.ext_count, + ]; + ::core::fmt::Formatter::debug_struct_fields_finish(f, "Attrs", names, values) + } + } + #[automatically_derived] + impl ::core::default::Default for Attrs { + #[inline] + fn default() -> Attrs { + Attrs { + size: ::core::default::Default::default(), + uid: ::core::default::Default::default(), + gid: ::core::default::Default::default(), + permissions: ::core::default::Default::default(), + atime: ::core::default::Default::default(), + mtime: ::core::default::Default::default(), + ext_count: ::core::default::Default::default(), + } + } + } + #[repr(u32)] + #[allow(non_camel_case_types)] + pub enum AttrsFlags { + SSH_FILEXFER_ATTR_SIZE = 0x01, + SSH_FILEXFER_ATTR_UIDGID = 0x02, + SSH_FILEXFER_ATTR_PERMISSIONS = 0x04, + SSH_FILEXFER_ATTR_ACMODTIME = 0x08, + SSH_FILEXFER_ATTR_EXTENDED = 0x80000000, + } + impl core::ops::AddAssign for u32 { + fn add_assign(&mut self, other: AttrsFlags) { + *self |= other as u32; + } + } + impl core::ops::BitAnd for u32 { + type Output = u32; + fn bitand(self, rhs: AttrsFlags) -> Self::Output { + self & rhs as u32 + } + } + impl Attrs { + pub fn flags(&self) -> u32 { + let mut flags: u32 = 0; + if self.size.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_SIZE; + } + if self.uid.is_some() || self.gid.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_UIDGID; + } + if self.permissions.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_PERMISSIONS; + } + if self.atime.is_some() || self.mtime.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_ACMODTIME; + } + flags + } + } + impl SSHEncode for Attrs { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + self.flags().enc(s)?; + if let Some(value) = self.size.as_ref() { + value.enc(s)? + } + if let Some(value) = self.uid.as_ref() { + value.enc(s)? + } + if let Some(value) = self.gid.as_ref() { + value.enc(s)? + } + if let Some(value) = self.permissions.as_ref() { + value.enc(s)? + } + if let Some(value) = self.atime.as_ref() { + value.enc(s)? + } + if let Some(value) = self.mtime.as_ref() { + value.enc(s)? + } + Ok(()) + } + } + impl<'de> SSHDecode<'de> for Attrs { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + let mut attrs = Attrs::default(); + let flags = u32::dec(s)? as u32; + if flags & AttrsFlags::SSH_FILEXFER_ATTR_SIZE != 0 { + attrs.size = Some(u64::dec(s)?); + } + if flags & AttrsFlags::SSH_FILEXFER_ATTR_UIDGID != 0 { + attrs.uid = Some(u32::dec(s)?); + attrs.gid = Some(u32::dec(s)?); + } + if flags & AttrsFlags::SSH_FILEXFER_ATTR_PERMISSIONS != 0 { + attrs.permissions = Some(u32::dec(s)?); + } + if flags & AttrsFlags::SSH_FILEXFER_ATTR_ACMODTIME != 0 { + attrs.atime = Some(u32::dec(s)?); + attrs.mtime = Some(u32::dec(s)?); + } + Ok(attrs) + } + } +} diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 0e5e6484..e44740e0 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,6 +1,6 @@ use sunset::error; use sunset::error::Error as SunsetError; -use sunset::packets::{MessageNumber, Packet}; +use sunset::packets::{MessageNumber, Packet, Unknown}; use sunset::sshwire::{ BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, WireResult, @@ -97,7 +97,7 @@ pub struct ReqId(pub u32); #[derive(Debug, SSHEncode, SSHDecode)] #[repr(u8)] #[allow(non_camel_case_types)] -pub enum StatusCode { +pub enum StatusCode<'a> { #[sshwire(variant = "ssh_fx_ok")] SSH_FX_OK = 0, #[sshwire(variant = "ssh_fx_eof")] @@ -116,8 +116,9 @@ pub enum StatusCode { SSH_FX_CONNECTION_LOST = 7, #[sshwire(variant = "ssh_fx_unsupported")] SSH_FX_OP_UNSUPPORTED = 8, - #[sshwire(variant = "ssh_fx_other")] - Other(u8), + // #[sshwire(variant = "ssh_fx_other")] + #[sshwire(unknown)] + Other(Unknown<'a>), } #[derive(Debug, SSHEncode, SSHDecode)] From 83198616852b86be062e11beeca24276f50953e7 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 25 Aug 2025 13:07:38 +1000 Subject: [PATCH 029/393] As per the pull request fails because of the edition, I am downgrading it 2024->2021 --- sftp/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 43385160..acd15ea5 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sunset-sftp" version = "0.1.0" -edition = "2024" +edition = "2021" [dependencies] sunset = { version = "0.3.0", path = "../" } From 1afb2235dd51304c016d0d8f46a2be44652b8be7 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 25 Aug 2025 13:16:46 +1000 Subject: [PATCH 030/393] Fixed unexpected intra-doc link error Most likely accidentally using the same format for the key list as intra-doc links --- src/namelist.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/namelist.rs b/src/namelist.rs index a793b246..c9bc90b4 100644 --- a/src/namelist.rs +++ b/src/namelist.rs @@ -24,7 +24,7 @@ use sshwire::{BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, WireResult}; /// Max count of LocalNames entries /// -/// Current max is for kex, [mlkem, curve25519, curve25519@libssh, ext-info, strictkex, kexguess2] +/// Current max is for kex: (mlkem, curve25519, curve25519@libssh, ext-info, strictkex, kexguess2) pub const MAX_LOCAL_NAMES: usize = 6; static EMPTY_LOCALNAMES: LocalNames = LocalNames::new(); From 998a317da99cec5d681e666d4d7f08bac7041139 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 25 Aug 2025 13:52:45 +1000 Subject: [PATCH 031/393] Clarifying that this implementation will use SFTP version 3 As it is the most common version in use today, ensuring broad compatibility. Support for other versions may be considered in the future. --- sftp/src/proto.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index e44740e0..a0af6721 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -15,6 +15,8 @@ pub struct Filename<'a>(TextString<'a>); #[derive(Debug, SSHEncode, SSHDecode)] struct FileHandle<'a>(pub BinString<'a>); +/// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 +const SFTP_VERSION: u32 = 3; #[derive(Debug, SSHEncode, SSHDecode)] pub struct InitVersion { // No ReqId for SSH_FXP_INIT From 786026fe260acf702a0864f058c41c49ecc84726 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 25 Aug 2025 15:58:16 +1000 Subject: [PATCH 032/393] Uncommenting macro_rules sftpmessages partially until problems arose with `Name` --- sftp/src/proto.rs | 145 +++++++++++++++++++++++----------------------- 1 file changed, 73 insertions(+), 72 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index a0af6721..8eda3a39 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -250,12 +250,12 @@ impl<'de> SSHDecode<'de> for Attrs { } } -// #[derive(Debug)] -// pub enum Error { -// UnknownPacket { number: u8 }, -// } +#[derive(Debug)] +pub enum Error { + UnknownPacket { number: u8 }, +} -// pub type Result = core::result::Result; +pub type Result = core::result::Result; // impl From for SunsetError { // fn from(error: Error) -> SunsetError { @@ -269,54 +269,62 @@ impl<'de> SSHDecode<'de> for Attrs { // } // } -// macro_rules! sftpmessages { -// ( -// $( ( $message_num:literal, -// $SpecificPacketVariant:ident, -// $SpecificPacketType:ty, -// $SSH_FXP_NAME:ident -// ), -// )* -// ) => { - -// #[derive(Debug, Clone)] -// #[repr(u8)] -// #[allow(non_camel_case_types)] -// pub enum SftpNum { -// // variants are eg -// // SSH_FXP_OPEN = 3, -// $( -// $SSH_FXP_NAME = $message_num, -// )* -// } +macro_rules! sftpmessages { + ( + $( ( $message_num:literal, + $SpecificPacketVariant:ident, + $SpecificPacketType:ty, + $SSH_FXP_NAME:ident + ), + )* + ) => { + #[derive(Debug, Clone)] + #[repr(u8)] + #[allow(non_camel_case_types)] + pub enum SftpNum { + // variants are eg + // SSH_FXP_OPEN = 3, + $( + $SSH_FXP_NAME = $message_num, + )* + } -// impl SftpNum { -// fn is_request(&self) -> bool { -// // TODO SSH_FXP_EXTENDED -// (2..=99).contains(&(self.clone() as u8)) -// } + impl SftpNum { + fn is_request(&self) -> bool { + // TODO SSH_FXP_EXTENDED + (2..=99).contains(&(self.clone() as u8)) + } -// fn is_response(&self) -> bool { -// // TODO SSH_FXP_EXTENDED_REPLY -// (100..=199).contains(&(self.clone() as u8)) -// } -// } + fn is_response(&self) -> bool { + // TODO SSH_FXP_EXTENDED_REPLY + (100..=199).contains(&(self.clone() as u8)) + } + } -// impl TryFrom for SftpNum { -// type Error = Error; -// fn try_from(v: u8) -> Result { -// match v { -// // eg -// // 3 => Ok(SftpNum::SSH_FXP_OPEN) -// $( -// $message_num => Ok(SftpNum::$SSH_FXP_NAME), -// )* -// _ => { -// Err(Error::UnknownPacket { number: v }) -// } -// } -// } -// } + impl TryFrom for SftpNum { + type Error = Error; + fn try_from(v: u8) -> Result { + match v { + // eg + // 3 => Ok(SftpNum::SSH_FXP_OPEN) + $( + $message_num => Ok(SftpNum::$SSH_FXP_NAME), + )* + _ => { + Err(Error::UnknownPacket { number: v }) + } + } + } + } + + // /// Top level SSH packet enum + // #[derive(Debug)] + // pub enum SftpPacket<'a> { + // // eg Open(Open<'a>), + // $( + // $SpecificPacketVariant($SpecificPacketType), + // )* + // } // impl SSHEncode for SftpPacket<'_> { // fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { @@ -359,14 +367,7 @@ impl<'de> SSHDecode<'de> for Attrs { // } // } -// /// Top level SSH packet enum -// #[derive(Debug)] -// pub enum SftpPacket<'a> { -// // eg Open(Open<'a>), -// $( -// $SpecificPacketVariant($SpecificPacketType), -// )* -// } + // impl<'a> SftpPacket<'a> { // pub fn sftp_num(&self) -> SftpNum { @@ -453,22 +454,22 @@ impl<'de> SSHDecode<'de> for Attrs { // } } // macro -// sftpmessages![ +sftpmessages![ -// // Message number ranges are also used by Sftpnum::is_request and is_response. +// Message number ranges are also used by Sftpnum::is_request and is_response. -// (1, Init, InitVersion, SSH_FXP_INIT), -// (2, Version, InitVersion, SSH_FXP_VERSION), +(1, Init, InitVersion, SSH_FXP_INIT), +(2, Version, InitVersion, SSH_FXP_VERSION), -// // Requests -// (3, Open, Open<'a>, SSH_FXP_OPEN), -// (4, Close, Close<'a>, SSH_FXP_CLOSE), -// (5, Read, Read<'a>, SSH_FXP_READ), +// Requests +(3, Open, Open<'a>, SSH_FXP_OPEN), +(4, Close, Close<'a>, SSH_FXP_CLOSE), +(5, Read, Read<'a>, SSH_FXP_READ), -// // Responses -// (101, Status, Status<'a>, SSH_FXP_STATUS), -// (102, Handle, Handle<'a>, SSH_FXP_HANDLE), -// (103, Data, Data<'a>, SSH_FXP_DATA), -// (104, Name, Name<'a>, SSH_FXP_NAME), +// Responses +(101, Status, Status<'a>, SSH_FXP_STATUS), +(102, Handle, Handle<'a>, SSH_FXP_HANDLE), +(103, Data, Data<'a>, SSH_FXP_DATA), +(104, Name, Name<'a>, SSH_FXP_NAME), -// ]; +]; From 39e72c3c1aa777e8c112281e850f599a83ca13d0 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 25 Aug 2025 15:59:58 +1000 Subject: [PATCH 033/393] Implementing SSHEncode and SSHDecode for Name Name has changed. It no longer contains count, but it Encodes it as the Names length --- sftp/src/proto.rs | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 8eda3a39..a67cc142 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -71,12 +71,6 @@ pub struct Data<'a> { pub data: BinString<'a>, } -#[derive(Debug, SSHEncode, SSHDecode)] -pub struct Name { - pub count: u32, - // TODO repeat NameEntry -} - #[derive(Debug, SSHEncode, SSHDecode)] pub struct NameEntry<'a> { pub filename: Filename<'a>, @@ -86,6 +80,37 @@ pub struct NameEntry<'a> { pub attrs: Attrs, } +#[derive(Debug)] +pub struct Name<'de>(pub Vec>); + +impl<'de> SSHDecode<'de> for Name<'de> { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + let count = u32::dec(s)? as usize; + + let mut names = Vec::with_capacity(count); + + for _ in 0..count { + names.push(NameEntry::dec(s)?); + } + + Ok(Name(names)) + } +} + +impl SSHEncode for Name<'_> { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + (self.0.len() as u32).enc(s)?; + + for element in self.0.iter() { + element.enc(s)?; + } + Ok(()) + } +} + #[derive(Debug, SSHEncode, SSHDecode)] pub struct ResponseAttributes { pub attrs: Attrs, From 2adfe1d240322672abe1c6fd93d772c28b8da0be Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 25 Aug 2025 16:02:31 +1000 Subject: [PATCH 034/393] adding macro_rules! sftpmessages closing curly brackets --- sftp/src/proto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index a67cc142..6aaf79e3 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -477,7 +477,7 @@ macro_rules! sftpmessages { // } // )* -// } } // macro +} } // macro sftpmessages![ From 05b92a56315c51316ad1be068e4505fbfc202652 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 25 Aug 2025 17:03:08 +1000 Subject: [PATCH 035/393] StatusCode: SSHEncode and SSHDecode Adding num_enum to sftp to handle the u32 to enum expansion --- Cargo.lock | 50 +++++++++++++++++++++++++++++++++++++++++------ sftp/Cargo.toml | 1 + sftp/src/proto.rs | 32 +++++++++++++++++++----------- 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80509a49..524ad539 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1812,11 +1812,12 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ - "num_enum_derive 0.7.3", + "num_enum_derive 0.7.4", + "rustversion", ] [[package]] @@ -1832,10 +1833,11 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.58", @@ -2011,7 +2013,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61d90fddc3d67f21bbf93683bc461b05d6a29c708caf3ffb79947d7ff7095406" dependencies = [ "arrayvec", - "num_enum 0.7.3", + "num_enum 0.7.4", "paste", ] @@ -2138,6 +2140,15 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -2880,6 +2891,7 @@ dependencies = [ name = "sunset-sftp" version = "0.1.0" dependencies = [ + "num_enum 0.7.4", "sunset", "sunset-sshwire-derive", ] @@ -3024,6 +3036,23 @@ dependencies = [ "syn 2.0.58", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "typenum" version = "1.17.0" @@ -3435,6 +3464,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + [[package]] name = "x25519-dalek" version = "2.0.1" diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index acd15ea5..7116ed01 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -4,5 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] +num_enum = {version = "0.7.4"} sunset = { version = "0.3.0", path = "../" } sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 6aaf79e3..8a6cada4 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,3 +1,4 @@ +use num_enum::{FromPrimitive, TryFromPrimitive}; use sunset::error; use sunset::error::Error as SunsetError; use sunset::packets::{MessageNumber, Packet, Unknown}; @@ -52,12 +53,12 @@ pub struct Write<'a> { // Responses -// #[derive(Debug, SSHEncode, SSHDecode)] -// pub struct Status<'a> { -// pub code: StatusCode, -// pub message: TextString<'a>, -// pub lang: TextString<'a>, -// } +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct Status<'a> { + pub code: StatusCode, + pub message: TextString<'a>, + pub lang: TextString<'a>, +} #[derive(Debug, SSHEncode, SSHDecode)] pub struct Handle<'a> { @@ -121,10 +122,10 @@ pub struct ResponseAttributes { #[derive(Debug, SSHEncode, SSHDecode, Clone, Copy)] pub struct ReqId(pub u32); -#[derive(Debug, SSHEncode, SSHDecode)] -#[repr(u8)] +#[derive(Debug, FromPrimitive, SSHEncode)] +#[repr(u32)] #[allow(non_camel_case_types)] -pub enum StatusCode<'a> { +pub enum StatusCode { #[sshwire(variant = "ssh_fx_ok")] SSH_FX_OK = 0, #[sshwire(variant = "ssh_fx_eof")] @@ -143,9 +144,18 @@ pub enum StatusCode<'a> { SSH_FX_CONNECTION_LOST = 7, #[sshwire(variant = "ssh_fx_unsupported")] SSH_FX_OP_UNSUPPORTED = 8, - // #[sshwire(variant = "ssh_fx_other")] #[sshwire(unknown)] - Other(Unknown<'a>), + #[num_enum(catch_all)] + Other(u32), +} + +impl<'de> SSHDecode<'de> for StatusCode { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + Ok(StatusCode::from(u32::dec(s)?)) + } } #[derive(Debug, SSHEncode, SSHDecode)] From e3c9fc9968a01ea83d10aa1b8b1ac22967ea33b0 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 26 Aug 2025 16:00:03 +1000 Subject: [PATCH 036/393] Bumping CI minimum version to 1.85 More on this version [here](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0/) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 581b1b2a..281bfca0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: # 1.83 is an arbitrary minimum, tested to notice when it bumps - rust_version: [stable, nightly, 1.83] + rust_version: [stable, nightly, 1.85] runs-on: ubuntu-latest env: RUSTUP_TOOLCHAIN: ${{ matrix.rust_version }} From 95618f2be02be67aa7896e8bf28fa5f573859441 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 26 Aug 2025 16:00:29 +1000 Subject: [PATCH 037/393] moving sftp edition to 2024 --- sftp/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 7116ed01..36c27867 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sunset-sftp" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] num_enum = {version = "0.7.4"} From 3f414c5f1d645b8d2dd9329a859f5691fc1f6e10 Mon Sep 17 00:00:00 2001 From: korbin Date: Tue, 26 Aug 2025 01:11:23 -0600 Subject: [PATCH 038/393] fix server pubkey verification --- src/servauth.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/servauth.rs b/src/servauth.rs index d4e4cd34..c415c2ac 100644 --- a/src/servauth.rs +++ b/src/servauth.rs @@ -130,7 +130,12 @@ impl ServAuth { // Extract the signature separately. The message for the signature // includes the auth packet without the signature part. let (key, sig) = match &mut p.method { - AuthMethod::PubKey(m) => (&m.pubkey.0, m.sig.take()), + AuthMethod::PubKey(m) => { + let sig = m.sig.take(); + // When we have a signature, we need to set force_sig=true so that the encoded message for verification has the boolean set correctly + m.force_sig = sig.is_some(); + (&m.pubkey.0, sig) + } _ => return Err(Error::bug()), }; From 60c5d494c24358c7add1100e764ece68b36cfd24 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 26 Aug 2025 16:00:03 +1000 Subject: [PATCH 039/393] Bumping CI minimum version to 1.85 More on this version [here](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0/) --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 581b1b2a..5a23e60f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,8 @@ jobs: all: strategy: matrix: - # 1.83 is an arbitrary minimum, tested to notice when it bumps - rust_version: [stable, nightly, 1.83] + # 1.85 is an arbitrary minimum, tested to notice when it bumps + rust_version: [stable, nightly, 1.85] runs-on: ubuntu-latest env: RUSTUP_TOOLCHAIN: ${{ matrix.rust_version }} From 286b2c64d488919a33be0d417ecedfe1edeb7383 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 11:54:28 +1000 Subject: [PATCH 040/393] Needed to add variants for SSHEncode/Decode for u64 --- src/sshwire.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/sshwire.rs b/src/sshwire.rs index bfd698ab..1733ee5d 100644 --- a/src/sshwire.rs +++ b/src/sshwire.rs @@ -486,6 +486,12 @@ impl SSHEncode for u32 { } } +impl SSHEncode for u64 { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + s.push(&self.to_be_bytes()) + } +} + // no length prefix impl SSHEncode for &[u8] { fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { @@ -555,6 +561,16 @@ impl<'de> SSHDecode<'de> for u32 { } } +impl<'de> SSHDecode<'de> for u64 { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + let t = s.take(core::mem::size_of::())?; + Ok(u64::from_be_bytes(t.try_into().unwrap())) + } +} + /// Decodes a SSH name string. Must be ASCII /// without control characters. RFC4251 section 6. pub fn try_as_ascii(t: &[u8]) -> WireResult<&AsciiStr> { From 3172bc855569939540cb4b9a628b3686f668cc03 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 25 Aug 2025 13:16:46 +1000 Subject: [PATCH 041/393] Fixed unexpected intra-doc link error Most likely accidentally using the same format for the key list as intra-doc links --- src/namelist.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/namelist.rs b/src/namelist.rs index a793b246..c9bc90b4 100644 --- a/src/namelist.rs +++ b/src/namelist.rs @@ -24,7 +24,7 @@ use sshwire::{BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, WireResult}; /// Max count of LocalNames entries /// -/// Current max is for kex, [mlkem, curve25519, curve25519@libssh, ext-info, strictkex, kexguess2] +/// Current max is for kex: (mlkem, curve25519, curve25519@libssh, ext-info, strictkex, kexguess2) pub const MAX_LOCAL_NAMES: usize = 6; static EMPTY_LOCALNAMES: LocalNames = LocalNames::new(); From fad8c69907c29f6cf5c134c137fd5d9efe72e057 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 27 Aug 2025 17:12:26 +1000 Subject: [PATCH 042/393] WIP: Starting to implement SftpPacket modifying SftpNum - modified macro pattern messsage_num from literal to tt (token tree) - lifetimes for SftpPacket are back - Added dependency to crate paste for pattern modification (to lower case but can be use for many other things) - I had to duplicate InitVersion to avoid repetitions of implementations in the macro expansion --- sftp/Cargo.toml | 1 + sftp/src/proto.rs | 163 ++++++++++++++++++++++++++-------------------- 2 files changed, 94 insertions(+), 70 deletions(-) diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 36c27867..b875c003 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -7,3 +7,4 @@ edition = "2024" num_enum = {version = "0.7.4"} sunset = { version = "0.3.0", path = "../" } sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } +paste = "1.0" \ No newline at end of file diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 8a6cada4..838e5398 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -18,13 +18,23 @@ struct FileHandle<'a>(pub BinString<'a>); /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 const SFTP_VERSION: u32 = 3; + +/// The SFTP version of the client #[derive(Debug, SSHEncode, SSHDecode)] -pub struct InitVersion { +pub struct InitVersionClient { // No ReqId for SSH_FXP_INIT pub version: u32, // TODO variable number of ExtPair } +/// The lowers SFTP version from the client and the server +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct InitVersionLowest { + // No ReqId for SSH_FXP_VERSION + pub version: u32, + // TODO variable number of ExtPair +} + #[derive(Debug, SSHEncode, SSHDecode)] pub struct Open<'a> { pub filename: Filename<'a>, @@ -306,61 +316,72 @@ pub type Result = core::result::Result; macro_rules! sftpmessages { ( - $( ( $message_num:literal, + $( ( $message_num:tt, $SpecificPacketVariant:ident, $SpecificPacketType:ty, $SSH_FXP_NAME:ident ), )* ) => { - #[derive(Debug, Clone)] - #[repr(u8)] - #[allow(non_camel_case_types)] - pub enum SftpNum { - // variants are eg - // SSH_FXP_OPEN = 3, - $( - $SSH_FXP_NAME = $message_num, - )* + paste::paste! { + #[derive(Debug, Clone, FromPrimitive, SSHEncode)] + #[repr(u8)] + #[allow(non_camel_case_types)] + pub enum SftpNum { + // SSH_FXP_OPEN = 3, + $( + #[sshwire(variant = $SSH_FXP_NAME:lower)] + $SSH_FXP_NAME = $message_num, + )* + #[sshwire(unknown)] + #[num_enum(catch_all)] + Other(u8), + } + } // paste + + impl<'de> SSHDecode<'de> for SftpNum { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + Ok(SftpNum::from(u8::dec(s)?)) + } + } + + impl From for u8{ + fn from(sftp_num: SftpNum) -> u8 { + match sftp_num { + $( + SftpNum::$SSH_FXP_NAME => $message_num, // TODO: Fix this + )* + _ => 0 // Other, not in the enum definition + + } + } + } impl SftpNum { fn is_request(&self) -> bool { // TODO SSH_FXP_EXTENDED - (2..=99).contains(&(self.clone() as u8)) + (2..=99).contains(&(u8::from(self.clone()))) } fn is_response(&self) -> bool { // TODO SSH_FXP_EXTENDED_REPLY - (100..=199).contains(&(self.clone() as u8)) + (100..=199).contains(&(u8::from(self.clone()))) } } - impl TryFrom for SftpNum { - type Error = Error; - fn try_from(v: u8) -> Result { - match v { - // eg - // 3 => Ok(SftpNum::SSH_FXP_OPEN) - $( - $message_num => Ok(SftpNum::$SSH_FXP_NAME), - )* - _ => { - Err(Error::UnknownPacket { number: v }) - } - } - } + /// Top level SSH packet enum + #[derive(Debug)] + pub enum SftpPacket<'a> { + // eg Open(Open<'a>), + $( + $SpecificPacketVariant($SpecificPacketType), + )* } - // /// Top level SSH packet enum - // #[derive(Debug)] - // pub enum SftpPacket<'a> { - // // eg Open(Open<'a>), - // $( - // $SpecificPacketVariant($SpecificPacketType), - // )* - // } - // impl SSHEncode for SftpPacket<'_> { // fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { // let t = self.message_num() as u8; @@ -402,21 +423,21 @@ macro_rules! sftpmessages { // } // } + impl<'a> SftpPacket<'a> { + /// Maps `SpecificPacketVariant` to `message_num` + pub fn sftp_num(&self) -> SftpNum { + match self { + // eg + // SftpPacket::Open(_) => { + // .. + $( + SftpPacket::$SpecificPacketVariant(_) => { - -// impl<'a> SftpPacket<'a> { -// pub fn sftp_num(&self) -> SftpNum { -// match self { -// // eg -// // SftpPacket::Open(_) => { -// // .. -// $( -// SftpPacket::$SpecificPacketVariant(_) => { -// MessageNumber::$SSH_FXP_NAME -// } -// )* -// } -// } + SftpNum::from($message_num as u8) + } + )* + } + } // /// Encode a request. // /// @@ -479,13 +500,15 @@ macro_rules! sftpmessages { // } // } -// $( -// impl<'a> From<$SpecificPacketType> for SftpPacket<'a> { -// fn from(s: $SpecificPacketType) -> SftpPacket<'a> { -// SftpPacket::$SpecificPacketVariant(s) -// } -// } -// )* + } + +$( +impl<'a> From<$SpecificPacketType> for SftpPacket<'a> { + fn from(s: $SpecificPacketType) -> SftpPacket<'a> { + SftpPacket::$SpecificPacketVariant(s) //find me + } +} +)* } } // macro @@ -493,18 +516,18 @@ sftpmessages![ // Message number ranges are also used by Sftpnum::is_request and is_response. -(1, Init, InitVersion, SSH_FXP_INIT), -(2, Version, InitVersion, SSH_FXP_VERSION), - -// Requests -(3, Open, Open<'a>, SSH_FXP_OPEN), -(4, Close, Close<'a>, SSH_FXP_CLOSE), -(5, Read, Read<'a>, SSH_FXP_READ), - -// Responses -(101, Status, Status<'a>, SSH_FXP_STATUS), -(102, Handle, Handle<'a>, SSH_FXP_HANDLE), -(103, Data, Data<'a>, SSH_FXP_DATA), -(104, Name, Name<'a>, SSH_FXP_NAME), +(1, Init, InitVersionClient, SSH_FXP_INIT), + (2, Version, InitVersionLowest, SSH_FXP_VERSION), + // Requests + (3, Open, Open<'a>, SSH_FXP_OPEN), + (4, Close, Close<'a>, SSH_FXP_CLOSE), + (5, Read, Read<'a>, SSH_FXP_READ), + (6, Write, Write<'a>, SSH_FXP_WRITE), + + // Responses + (101, Status, Status<'a>, SSH_FXP_STATUS), + (102, Handle, Handle<'a>, SSH_FXP_HANDLE), + (103, Data, Data<'a>, SSH_FXP_DATA), + (104, Name, Name<'a>, SSH_FXP_NAME), ]; From 8d5562865eebae1ca02497ec49c3dec47b449ed4 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 27 Aug 2025 18:23:57 +1000 Subject: [PATCH 043/393] Adding SSHEncode and SSHDecode for SftpPacket --- sftp/src/proto.rs | 80 ++++++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 838e5398..037f0a20 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -374,6 +374,9 @@ macro_rules! sftpmessages { } /// Top level SSH packet enum + /// + /// It helps identifying the SFTP Packet type and handling it accordingly + /// This is done using the SFTP field type #[derive(Debug)] pub enum SftpPacket<'a> { // eg Open(Open<'a>), @@ -382,46 +385,45 @@ macro_rules! sftpmessages { )* } -// impl SSHEncode for SftpPacket<'_> { -// fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { -// let t = self.message_num() as u8; -// t.enc(s)?; -// match self { -// // eg -// // Packet::KexInit(p) => { -// // ... -// $( -// Packet::$SpecificPacketVariant(p) => { -// p.enc(s)? -// } -// )* -// }; -// Ok(()) -// } -// } + impl SSHEncode for SftpPacket<'_> { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + let t = u8::from(self.sftp_num()); + t.enc(s)?; + match self { + // eg + // SftpPacket::KexInit(p) => { + // ... + $( + SftpPacket::$SpecificPacketVariant(p) => { + p.enc(s)? + } + )* + }; + Ok(()) + } + } -// impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { -// fn dec(s: &mut S) -> WireResult -// where S: SSHSource<'de> { -// let msg_num = u8::dec(s)?; -// let ty = MessageNumber::try_from(msg_num); -// let ty = match ty { -// Ok(t) => t, -// Err(_) => return Err(WireError::UnknownPacket { number: msg_num }) -// }; - -// // Decode based on the message number -// let p = match ty { -// // eg -// // MessageNumber::SSH_MSG_KEXINIT => Packet::KexInit( -// // ... -// $( -// MessageNumber::$SSH_FXP_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), -// )* -// }; -// Ok(p) -// } -// } + impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> + where 'a: 'de, + { + fn dec(s: &mut S) -> WireResult + where S: SSHSource<'de> { + let packet_type_number = u8::dec(s)?; + + let packet_type = SftpNum::from(packet_type_number); + + let decoded_packet = match packet_type { + $( + SftpNum::$SSH_FXP_NAME => { + let inner_type = <$SpecificPacketType>::dec(s)?; + SftpPacket::$SpecificPacketVariant(inner_type) + }, + )* + _ => return Err(WireError::UnknownPacket { number: packet_type_number }) + }; + Ok(decoded_packet) + } + } impl<'a> SftpPacket<'a> { /// Maps `SpecificPacketVariant` to `message_num` From 2f095250ac2ea185ba422e1e0e111c62942d0a93 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 27 Aug 2025 18:23:57 +1000 Subject: [PATCH 044/393] Adding SSHEncode and SSHDecode for SftpPacket Updating cargo-expand-sftp.rs too --- sftp/out/cargo-expand-sftp.rs | 710 ++++++++++++++++++++++++++++++---- sftp/src/proto.rs | 80 ++-- 2 files changed, 681 insertions(+), 109 deletions(-) diff --git a/sftp/out/cargo-expand-sftp.rs b/sftp/out/cargo-expand-sftp.rs index b2766361..bbdd7f37 100644 --- a/sftp/out/cargo-expand-sftp.rs +++ b/sftp/out/cargo-expand-sftp.rs @@ -1,9 +1,10 @@ #![feature(prelude_import)] -#[macro_use] -extern crate std; #[prelude_import] use std::prelude::rust_2024::*; +#[macro_use] +extern crate std; mod proto { + use num_enum::FromPrimitive; use sunset::error; use sunset::error::Error as SunsetError; use sunset::packets::{MessageNumber, Packet, Unknown}; @@ -66,22 +67,25 @@ mod proto { Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) } } - pub struct InitVersion { + /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 + const SFTP_VERSION: u32 = 3; + /// The SFTP version of the client + pub struct InitVersionClient { pub version: u32, } #[automatically_derived] - impl ::core::fmt::Debug for InitVersion { + impl ::core::fmt::Debug for InitVersionClient { #[inline] fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { ::core::fmt::Formatter::debug_struct_field1_finish( f, - "InitVersion", + "InitVersionClient", "version", &&self.version, ) } } - impl ::sunset::sshwire::SSHEncode for InitVersion { + impl ::sunset::sshwire::SSHEncode for InitVersionClient { fn enc( &self, s: &mut dyn ::sunset::sshwire::SSHSink, @@ -90,7 +94,40 @@ mod proto { Ok(()) } } - impl<'de> ::sunset::sshwire::SSHDecode<'de> for InitVersion { + impl<'de> ::sunset::sshwire::SSHDecode<'de> for InitVersionClient { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_version = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { version: field_version }) + } + } + /// The lowers SFTP version from the client and the server + pub struct InitVersionLowest { + pub version: u32, + } + #[automatically_derived] + impl ::core::fmt::Debug for InitVersionLowest { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "InitVersionLowest", + "version", + &&self.version, + ) + } + } + impl ::sunset::sshwire::SSHEncode for InitVersionLowest { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.version, s)?; + Ok(()) + } + } + impl<'de> ::sunset::sshwire::SSHDecode<'de> for InitVersionLowest { fn dec>( s: &mut S, ) -> ::sunset::sshwire::WireResult { @@ -280,6 +317,55 @@ mod proto { }) } } + pub struct Status<'a> { + pub code: StatusCode, + pub message: TextString<'a>, + pub lang: TextString<'a>, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Status<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field3_finish( + f, + "Status", + "code", + &self.code, + "message", + &self.message, + "lang", + &&self.lang, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Status<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.code, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.message, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.lang, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Status<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_code = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_message = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_lang = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { + code: field_code, + message: field_message, + lang: field_lang, + }) + } + } pub struct Handle<'a> { pub handle: FileHandle<'a>, } @@ -364,38 +450,6 @@ mod proto { }) } } - pub struct Name { - pub count: u32, - } - #[automatically_derived] - impl ::core::fmt::Debug for Name { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "Name", - "count", - &&self.count, - ) - } - } - impl ::sunset::sshwire::SSHEncode for Name { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.count, s)?; - Ok(()) - } - } - impl<'de> ::sunset::sshwire::SSHDecode<'de> for Name { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_count = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { count: field_count }) - } - } pub struct NameEntry<'a> { pub filename: Filename<'a>, /// longname is an undefined text line like "ls -l", @@ -447,6 +501,36 @@ mod proto { }) } } + pub struct Name<'de>(pub Vec>); + #[automatically_derived] + impl<'de> ::core::fmt::Debug for Name<'de> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Name", &&self.0) + } + } + impl<'de> SSHDecode<'de> for Name<'de> { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + let count = u32::dec(s)? as usize; + let mut names = Vec::with_capacity(count); + for _ in 0..count { + names.push(NameEntry::dec(s)?); + } + Ok(Name(names)) + } + } + impl SSHEncode for Name<'_> { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + (self.0.len() as u32).enc(s)?; + for element in self.0.iter() { + element.enc(s)?; + } + Ok(()) + } + } pub struct ResponseAttributes { pub attrs: Attrs, } @@ -513,9 +597,9 @@ mod proto { } #[automatically_derived] impl ::core::marker::Copy for ReqId {} - #[repr(u8)] + #[repr(u32)] #[allow(non_camel_case_types)] - pub enum StatusCode<'a> { + pub enum StatusCode { #[sshwire(variant = "ssh_fx_ok")] SSH_FX_OK = 0, #[sshwire(variant = "ssh_fx_eof")] @@ -535,11 +619,12 @@ mod proto { #[sshwire(variant = "ssh_fx_unsupported")] SSH_FX_OP_UNSUPPORTED = 8, #[sshwire(unknown)] - Other(Unknown<'a>), + #[num_enum(catch_all)] + Other(u32), } #[automatically_derived] #[allow(non_camel_case_types)] - impl<'a> ::core::fmt::Debug for StatusCode<'a> { + impl ::core::fmt::Debug for StatusCode { #[inline] fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { match self { @@ -580,7 +665,44 @@ mod proto { } } } - impl<'a> ::sunset::sshwire::SSHEncode for StatusCode<'a> { + impl ::num_enum::FromPrimitive for StatusCode { + type Primitive = u32; + fn from_primitive(number: Self::Primitive) -> Self { + #![allow(non_upper_case_globals)] + const SSH_FX_OK__num_enum_0__: u32 = 0; + const SSH_FX_EOF__num_enum_0__: u32 = 1; + const SSH_FX_NO_SUCH_FILE__num_enum_0__: u32 = 2; + const SSH_FX_PERMISSION_DENIED__num_enum_0__: u32 = 3; + const SSH_FX_FAILURE__num_enum_0__: u32 = 4; + const SSH_FX_BAD_MESSAGE__num_enum_0__: u32 = 5; + const SSH_FX_NO_CONNECTION__num_enum_0__: u32 = 6; + const SSH_FX_CONNECTION_LOST__num_enum_0__: u32 = 7; + const SSH_FX_OP_UNSUPPORTED__num_enum_0__: u32 = 8; + #[deny(unreachable_patterns)] + match number { + SSH_FX_OK__num_enum_0__ => Self::SSH_FX_OK, + SSH_FX_EOF__num_enum_0__ => Self::SSH_FX_EOF, + SSH_FX_NO_SUCH_FILE__num_enum_0__ => Self::SSH_FX_NO_SUCH_FILE, + SSH_FX_PERMISSION_DENIED__num_enum_0__ => Self::SSH_FX_PERMISSION_DENIED, + SSH_FX_FAILURE__num_enum_0__ => Self::SSH_FX_FAILURE, + SSH_FX_BAD_MESSAGE__num_enum_0__ => Self::SSH_FX_BAD_MESSAGE, + SSH_FX_NO_CONNECTION__num_enum_0__ => Self::SSH_FX_NO_CONNECTION, + SSH_FX_CONNECTION_LOST__num_enum_0__ => Self::SSH_FX_CONNECTION_LOST, + SSH_FX_OP_UNSUPPORTED__num_enum_0__ => Self::SSH_FX_OP_UNSUPPORTED, + #[allow(unreachable_patterns)] + _ => Self::Other(number), + } + } + } + impl ::core::convert::From for StatusCode { + #[inline] + fn from(number: u32) -> Self { + ::num_enum::FromPrimitive::from_primitive(number) + } + } + #[doc(hidden)] + impl ::num_enum::CannotDeriveBothFromPrimitiveAndTryFromPrimitive for StatusCode {} + impl ::sunset::sshwire::SSHEncode for StatusCode { fn enc( &self, s: &mut dyn ::sunset::sshwire::SSHSink, @@ -602,7 +724,7 @@ mod proto { #[allow(unreachable_code)] Ok(()) } } - impl<'a> ::sunset::sshwire::SSHEncodeEnum for StatusCode<'a> { + impl ::sunset::sshwire::SSHEncodeEnum for StatusCode { fn variant_name(&self) -> ::sunset::sshwire::WireResult<&'static str> { let r = match self { Self::SSH_FX_OK => "ssh_fx_ok", @@ -621,31 +743,12 @@ mod proto { #[allow(unreachable_code)] Ok(r) } } - impl<'de, 'a> ::sunset::sshwire::SSHDecodeEnum<'de> for StatusCode<'a> - where - 'de: 'a, - { - fn dec_enum>( - s: &mut S, - variant: &'de [u8], - ) -> ::sunset::sshwire::WireResult { - let var_str = ::sunset::sshwire::try_as_ascii_str(variant).ok(); - let r = match var_str { - Some("ssh_fx_ok") => Self::SSH_FX_OK, - Some("ssh_fx_eof") => Self::SSH_FX_EOF, - Some("ssh_fx_no_such_file") => Self::SSH_FX_NO_SUCH_FILE, - Some("ssh_fx_permission_denied") => Self::SSH_FX_PERMISSION_DENIED, - Some("ssh_fx_failure") => Self::SSH_FX_FAILURE, - Some("ssh_fx_bad_message") => Self::SSH_FX_BAD_MESSAGE, - Some("ssh_fx_no_connection") => Self::SSH_FX_NO_CONNECTION, - Some("ssh_fx_connection_lost") => Self::SSH_FX_CONNECTION_LOST, - Some("ssh_fx_unsupported") => Self::SSH_FX_OP_UNSUPPORTED, - _ => { - s.ctx().seen_unknown = true; - Self::Other(Unknown::new(variant)) - } - }; - Ok(r) + impl<'de> SSHDecode<'de> for StatusCode { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + Ok(StatusCode::from(u32::dec(s)?)) } } pub struct ExtPair<'a> { @@ -826,4 +929,471 @@ mod proto { Ok(attrs) } } + pub enum Error { + UnknownPacket { number: u8 }, + } + #[automatically_derived] + impl ::core::fmt::Debug for Error { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + Error::UnknownPacket { number: __self_0 } => { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "UnknownPacket", + "number", + &__self_0, + ) + } + } + } + } + pub type Result = core::result::Result; + #[repr(u8)] + #[allow(non_camel_case_types)] + pub enum SftpNum { + #[sshwire(variant = "ssh_fxp_init")] + SSH_FXP_INIT = 1, + #[sshwire(variant = "ssh_fxp_version")] + SSH_FXP_VERSION = 2, + #[sshwire(variant = "ssh_fxp_open")] + SSH_FXP_OPEN = 3, + #[sshwire(variant = "ssh_fxp_close")] + SSH_FXP_CLOSE = 4, + #[sshwire(variant = "ssh_fxp_read")] + SSH_FXP_READ = 5, + #[sshwire(variant = "ssh_fxp_write")] + SSH_FXP_WRITE = 6, + #[sshwire(variant = "ssh_fxp_status")] + SSH_FXP_STATUS = 101, + #[sshwire(variant = "ssh_fxp_handle")] + SSH_FXP_HANDLE = 102, + #[sshwire(variant = "ssh_fxp_data")] + SSH_FXP_DATA = 103, + #[sshwire(variant = "ssh_fxp_name")] + SSH_FXP_NAME = 104, + #[sshwire(unknown)] + #[num_enum(catch_all)] + Other(u8), + } + #[automatically_derived] + #[allow(non_camel_case_types)] + impl ::core::fmt::Debug for SftpNum { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + SftpNum::SSH_FXP_INIT => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_INIT") + } + SftpNum::SSH_FXP_VERSION => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_VERSION") + } + SftpNum::SSH_FXP_OPEN => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_OPEN") + } + SftpNum::SSH_FXP_CLOSE => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_CLOSE") + } + SftpNum::SSH_FXP_READ => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_READ") + } + SftpNum::SSH_FXP_WRITE => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_WRITE") + } + SftpNum::SSH_FXP_STATUS => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_STATUS") + } + SftpNum::SSH_FXP_HANDLE => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_HANDLE") + } + SftpNum::SSH_FXP_DATA => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_DATA") + } + SftpNum::SSH_FXP_NAME => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_NAME") + } + SftpNum::Other(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Other", + &__self_0, + ) + } + } + } + } + #[automatically_derived] + #[allow(non_camel_case_types)] + impl ::core::clone::Clone for SftpNum { + #[inline] + fn clone(&self) -> SftpNum { + match self { + SftpNum::SSH_FXP_INIT => SftpNum::SSH_FXP_INIT, + SftpNum::SSH_FXP_VERSION => SftpNum::SSH_FXP_VERSION, + SftpNum::SSH_FXP_OPEN => SftpNum::SSH_FXP_OPEN, + SftpNum::SSH_FXP_CLOSE => SftpNum::SSH_FXP_CLOSE, + SftpNum::SSH_FXP_READ => SftpNum::SSH_FXP_READ, + SftpNum::SSH_FXP_WRITE => SftpNum::SSH_FXP_WRITE, + SftpNum::SSH_FXP_STATUS => SftpNum::SSH_FXP_STATUS, + SftpNum::SSH_FXP_HANDLE => SftpNum::SSH_FXP_HANDLE, + SftpNum::SSH_FXP_DATA => SftpNum::SSH_FXP_DATA, + SftpNum::SSH_FXP_NAME => SftpNum::SSH_FXP_NAME, + SftpNum::Other(__self_0) => { + SftpNum::Other(::core::clone::Clone::clone(__self_0)) + } + } + } + } + impl ::num_enum::FromPrimitive for SftpNum { + type Primitive = u8; + fn from_primitive(number: Self::Primitive) -> Self { + #![allow(non_upper_case_globals)] + const SSH_FXP_INIT__num_enum_0__: u8 = 1; + const SSH_FXP_VERSION__num_enum_0__: u8 = 2; + const SSH_FXP_OPEN__num_enum_0__: u8 = 3; + const SSH_FXP_CLOSE__num_enum_0__: u8 = 4; + const SSH_FXP_READ__num_enum_0__: u8 = 5; + const SSH_FXP_WRITE__num_enum_0__: u8 = 6; + const SSH_FXP_STATUS__num_enum_0__: u8 = 101; + const SSH_FXP_HANDLE__num_enum_0__: u8 = 102; + const SSH_FXP_DATA__num_enum_0__: u8 = 103; + const SSH_FXP_NAME__num_enum_0__: u8 = 104; + #[deny(unreachable_patterns)] + match number { + SSH_FXP_INIT__num_enum_0__ => Self::SSH_FXP_INIT, + SSH_FXP_VERSION__num_enum_0__ => Self::SSH_FXP_VERSION, + SSH_FXP_OPEN__num_enum_0__ => Self::SSH_FXP_OPEN, + SSH_FXP_CLOSE__num_enum_0__ => Self::SSH_FXP_CLOSE, + SSH_FXP_READ__num_enum_0__ => Self::SSH_FXP_READ, + SSH_FXP_WRITE__num_enum_0__ => Self::SSH_FXP_WRITE, + SSH_FXP_STATUS__num_enum_0__ => Self::SSH_FXP_STATUS, + SSH_FXP_HANDLE__num_enum_0__ => Self::SSH_FXP_HANDLE, + SSH_FXP_DATA__num_enum_0__ => Self::SSH_FXP_DATA, + SSH_FXP_NAME__num_enum_0__ => Self::SSH_FXP_NAME, + #[allow(unreachable_patterns)] + _ => Self::Other(number), + } + } + } + impl ::core::convert::From for SftpNum { + #[inline] + fn from(number: u8) -> Self { + ::num_enum::FromPrimitive::from_primitive(number) + } + } + #[doc(hidden)] + impl ::num_enum::CannotDeriveBothFromPrimitiveAndTryFromPrimitive for SftpNum {} + impl ::sunset::sshwire::SSHEncode for SftpNum { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + match *self { + Self::SSH_FXP_INIT => {} + Self::SSH_FXP_VERSION => {} + Self::SSH_FXP_OPEN => {} + Self::SSH_FXP_CLOSE => {} + Self::SSH_FXP_READ => {} + Self::SSH_FXP_WRITE => {} + Self::SSH_FXP_STATUS => {} + Self::SSH_FXP_HANDLE => {} + Self::SSH_FXP_DATA => {} + Self::SSH_FXP_NAME => {} + Self::Other(ref i) => { + return Err(::sunset::sshwire::WireError::UnknownVariant); + } + } + #[allow(unreachable_code)] Ok(()) + } + } + impl ::sunset::sshwire::SSHEncodeEnum for SftpNum { + fn variant_name(&self) -> ::sunset::sshwire::WireResult<&'static str> { + let r = match self { + Self::SSH_FXP_INIT => "ssh_fxp_init", + Self::SSH_FXP_VERSION => "ssh_fxp_version", + Self::SSH_FXP_OPEN => "ssh_fxp_open", + Self::SSH_FXP_CLOSE => "ssh_fxp_close", + Self::SSH_FXP_READ => "ssh_fxp_read", + Self::SSH_FXP_WRITE => "ssh_fxp_write", + Self::SSH_FXP_STATUS => "ssh_fxp_status", + Self::SSH_FXP_HANDLE => "ssh_fxp_handle", + Self::SSH_FXP_DATA => "ssh_fxp_data", + Self::SSH_FXP_NAME => "ssh_fxp_name", + Self::Other(_) => { + return Err(::sunset::sshwire::WireError::UnknownVariant); + } + }; + #[allow(unreachable_code)] Ok(r) + } + } + impl<'de> SSHDecode<'de> for SftpNum { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + Ok(SftpNum::from(u8::dec(s)?)) + } + } + impl From for u8 { + fn from(sftp_num: SftpNum) -> u8 { + match sftp_num { + SftpNum::SSH_FXP_INIT => 1, + SftpNum::SSH_FXP_VERSION => 2, + SftpNum::SSH_FXP_OPEN => 3, + SftpNum::SSH_FXP_CLOSE => 4, + SftpNum::SSH_FXP_READ => 5, + SftpNum::SSH_FXP_WRITE => 6, + SftpNum::SSH_FXP_STATUS => 101, + SftpNum::SSH_FXP_HANDLE => 102, + SftpNum::SSH_FXP_DATA => 103, + SftpNum::SSH_FXP_NAME => 104, + _ => 0, + } + } + } + impl SftpNum { + fn is_request(&self) -> bool { + (2..=99).contains(&(u8::from(self.clone()))) + } + fn is_response(&self) -> bool { + (100..=199).contains(&(u8::from(self.clone()))) + } + } + /// Top level SSH packet enum + /// + /// It helps identifying the SFTP Packet type and handling it accordingly + /// This is done using the SFTP field type + pub enum SftpPacket<'a> { + Init(InitVersionClient), + Version(InitVersionLowest), + Open(Open<'a>), + Close(Close<'a>), + Read(Read<'a>), + Write(Write<'a>), + Status(Status<'a>), + Handle(Handle<'a>), + Data(Data<'a>), + Name(Name<'a>), + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for SftpPacket<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + SftpPacket::Init(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Init", + &__self_0, + ) + } + SftpPacket::Version(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Version", + &__self_0, + ) + } + SftpPacket::Open(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Open", + &__self_0, + ) + } + SftpPacket::Close(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Close", + &__self_0, + ) + } + SftpPacket::Read(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Read", + &__self_0, + ) + } + SftpPacket::Write(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Write", + &__self_0, + ) + } + SftpPacket::Status(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Status", + &__self_0, + ) + } + SftpPacket::Handle(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Handle", + &__self_0, + ) + } + SftpPacket::Data(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Data", + &__self_0, + ) + } + SftpPacket::Name(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Name", + &__self_0, + ) + } + } + } + } + impl SSHEncode for SftpPacket<'_> { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + let t = u8::from(self.sftp_num()); + t.enc(s)?; + match self { + SftpPacket::Init(p) => p.enc(s)?, + SftpPacket::Version(p) => p.enc(s)?, + SftpPacket::Open(p) => p.enc(s)?, + SftpPacket::Close(p) => p.enc(s)?, + SftpPacket::Read(p) => p.enc(s)?, + SftpPacket::Write(p) => p.enc(s)?, + SftpPacket::Status(p) => p.enc(s)?, + SftpPacket::Handle(p) => p.enc(s)?, + SftpPacket::Data(p) => p.enc(s)?, + SftpPacket::Name(p) => p.enc(s)?, + }; + Ok(()) + } + } + impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + let packet_type_number = u8::dec(s)?; + let packet_type = SftpNum::from(packet_type_number); + let decoded_packet = match packet_type { + SSH_FXP_INIT => { + let inner_type = ::dec(s)?; + SftpPacket::Init(inner_type) + } + SSH_FXP_VERSION => { + let inner_type = ::dec(s)?; + SftpPacket::Version(inner_type) + } + SSH_FXP_OPEN => { + let inner_type = >::dec(s)?; + SftpPacket::Open(inner_type) + } + SSH_FXP_CLOSE => { + let inner_type = >::dec(s)?; + SftpPacket::Close(inner_type) + } + SSH_FXP_READ => { + let inner_type = >::dec(s)?; + SftpPacket::Read(inner_type) + } + SSH_FXP_WRITE => { + let inner_type = >::dec(s)?; + SftpPacket::Write(inner_type) + } + SSH_FXP_STATUS => { + let inner_type = >::dec(s)?; + SftpPacket::Status(inner_type) + } + SSH_FXP_HANDLE => { + let inner_type = >::dec(s)?; + SftpPacket::Handle(inner_type) + } + SSH_FXP_DATA => { + let inner_type = >::dec(s)?; + SftpPacket::Data(inner_type) + } + SSH_FXP_NAME => { + let inner_type = >::dec(s)?; + SftpPacket::Name(inner_type) + } + _ => { + return Err(WireError::UnknownPacket { + number: packet_type_number, + }); + } + }; + Ok(decoded_packet) + } + } + impl<'a> SftpPacket<'a> { + /// Maps `SpecificPacketVariant` to `message_num` + pub fn sftp_num(&self) -> SftpNum { + match self { + SftpPacket::Init(_) => SftpNum::from(1 as u8), + SftpPacket::Version(_) => SftpNum::from(2 as u8), + SftpPacket::Open(_) => SftpNum::from(3 as u8), + SftpPacket::Close(_) => SftpNum::from(4 as u8), + SftpPacket::Read(_) => SftpNum::from(5 as u8), + SftpPacket::Write(_) => SftpNum::from(6 as u8), + SftpPacket::Status(_) => SftpNum::from(101 as u8), + SftpPacket::Handle(_) => SftpNum::from(102 as u8), + SftpPacket::Data(_) => SftpNum::from(103 as u8), + SftpPacket::Name(_) => SftpNum::from(104 as u8), + } + } + } + impl<'a> From for SftpPacket<'a> { + fn from(s: InitVersionClient) -> SftpPacket<'a> { + SftpPacket::Init(s) + } + } + impl<'a> From for SftpPacket<'a> { + fn from(s: InitVersionLowest) -> SftpPacket<'a> { + SftpPacket::Version(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Open<'a>) -> SftpPacket<'a> { + SftpPacket::Open(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Close<'a>) -> SftpPacket<'a> { + SftpPacket::Close(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Read<'a>) -> SftpPacket<'a> { + SftpPacket::Read(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Write<'a>) -> SftpPacket<'a> { + SftpPacket::Write(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Status<'a>) -> SftpPacket<'a> { + SftpPacket::Status(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Handle<'a>) -> SftpPacket<'a> { + SftpPacket::Handle(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Data<'a>) -> SftpPacket<'a> { + SftpPacket::Data(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Name<'a>) -> SftpPacket<'a> { + SftpPacket::Name(s) + } + } } diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 838e5398..037f0a20 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -374,6 +374,9 @@ macro_rules! sftpmessages { } /// Top level SSH packet enum + /// + /// It helps identifying the SFTP Packet type and handling it accordingly + /// This is done using the SFTP field type #[derive(Debug)] pub enum SftpPacket<'a> { // eg Open(Open<'a>), @@ -382,46 +385,45 @@ macro_rules! sftpmessages { )* } -// impl SSHEncode for SftpPacket<'_> { -// fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { -// let t = self.message_num() as u8; -// t.enc(s)?; -// match self { -// // eg -// // Packet::KexInit(p) => { -// // ... -// $( -// Packet::$SpecificPacketVariant(p) => { -// p.enc(s)? -// } -// )* -// }; -// Ok(()) -// } -// } + impl SSHEncode for SftpPacket<'_> { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + let t = u8::from(self.sftp_num()); + t.enc(s)?; + match self { + // eg + // SftpPacket::KexInit(p) => { + // ... + $( + SftpPacket::$SpecificPacketVariant(p) => { + p.enc(s)? + } + )* + }; + Ok(()) + } + } -// impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { -// fn dec(s: &mut S) -> WireResult -// where S: SSHSource<'de> { -// let msg_num = u8::dec(s)?; -// let ty = MessageNumber::try_from(msg_num); -// let ty = match ty { -// Ok(t) => t, -// Err(_) => return Err(WireError::UnknownPacket { number: msg_num }) -// }; - -// // Decode based on the message number -// let p = match ty { -// // eg -// // MessageNumber::SSH_MSG_KEXINIT => Packet::KexInit( -// // ... -// $( -// MessageNumber::$SSH_FXP_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), -// )* -// }; -// Ok(p) -// } -// } + impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> + where 'a: 'de, + { + fn dec(s: &mut S) -> WireResult + where S: SSHSource<'de> { + let packet_type_number = u8::dec(s)?; + + let packet_type = SftpNum::from(packet_type_number); + + let decoded_packet = match packet_type { + $( + SftpNum::$SSH_FXP_NAME => { + let inner_type = <$SpecificPacketType>::dec(s)?; + SftpPacket::$SpecificPacketVariant(inner_type) + }, + )* + _ => return Err(WireError::UnknownPacket { number: packet_type_number }) + }; + Ok(decoded_packet) + } + } impl<'a> SftpPacket<'a> { /// Maps `SpecificPacketVariant` to `message_num` From 93117ec7bfdc8540c8bc986935d7ad44338ee609 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 28 Aug 2025 13:44:56 +1000 Subject: [PATCH 045/393] Added past dependency in Cargo.lock and as use --- Cargo.lock | 1 + sftp/src/proto.rs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 524ad539..8dcaffa8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2892,6 +2892,7 @@ name = "sunset-sftp" version = "0.1.0" dependencies = [ "num_enum 0.7.4", + "paste", "sunset", "sunset-sshwire-derive", ] diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 037f0a20..0018e620 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,4 +1,5 @@ -use num_enum::{FromPrimitive, TryFromPrimitive}; +use num_enum::FromPrimitive; +use paste::paste; use sunset::error; use sunset::error::Error as SunsetError; use sunset::packets::{MessageNumber, Packet, Unknown}; @@ -323,7 +324,7 @@ macro_rules! sftpmessages { ), )* ) => { - paste::paste! { + paste! { #[derive(Debug, Clone, FromPrimitive, SSHEncode)] #[repr(u8)] #[allow(non_camel_case_types)] From fab799627f96f879df874ae016182937c3bd5f1e Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 28 Aug 2025 13:47:29 +1000 Subject: [PATCH 046/393] Restricting SSHDecode for SftpPacket lifetimes so they match It is convoluted, changing unifying lifetimes for this implementation to `'a` would work as well --- sftp/out/cargo-expand-sftp.rs | 26 +++++++++++++++----------- sftp/src/proto.rs | 5 +++-- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/sftp/out/cargo-expand-sftp.rs b/sftp/out/cargo-expand-sftp.rs index bbdd7f37..996d330f 100644 --- a/sftp/out/cargo-expand-sftp.rs +++ b/sftp/out/cargo-expand-sftp.rs @@ -5,6 +5,7 @@ use std::prelude::rust_2024::*; extern crate std; mod proto { use num_enum::FromPrimitive; + use paste::paste; use sunset::error; use sunset::error::Error as SunsetError; use sunset::packets::{MessageNumber, Packet, Unknown}; @@ -1272,7 +1273,10 @@ mod proto { Ok(()) } } - impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { + impl<'a: 'de, 'de> SSHDecode<'de> for SftpPacket<'a> + where + 'de: 'a, + { fn dec(s: &mut S) -> WireResult where S: SSHSource<'de>, @@ -1280,43 +1284,43 @@ mod proto { let packet_type_number = u8::dec(s)?; let packet_type = SftpNum::from(packet_type_number); let decoded_packet = match packet_type { - SSH_FXP_INIT => { + SftpNum::SSH_FXP_INIT => { let inner_type = ::dec(s)?; SftpPacket::Init(inner_type) } - SSH_FXP_VERSION => { + SftpNum::SSH_FXP_VERSION => { let inner_type = ::dec(s)?; SftpPacket::Version(inner_type) } - SSH_FXP_OPEN => { + SftpNum::SSH_FXP_OPEN => { let inner_type = >::dec(s)?; SftpPacket::Open(inner_type) } - SSH_FXP_CLOSE => { + SftpNum::SSH_FXP_CLOSE => { let inner_type = >::dec(s)?; SftpPacket::Close(inner_type) } - SSH_FXP_READ => { + SftpNum::SSH_FXP_READ => { let inner_type = >::dec(s)?; SftpPacket::Read(inner_type) } - SSH_FXP_WRITE => { + SftpNum::SSH_FXP_WRITE => { let inner_type = >::dec(s)?; SftpPacket::Write(inner_type) } - SSH_FXP_STATUS => { + SftpNum::SSH_FXP_STATUS => { let inner_type = >::dec(s)?; SftpPacket::Status(inner_type) } - SSH_FXP_HANDLE => { + SftpNum::SSH_FXP_HANDLE => { let inner_type = >::dec(s)?; SftpPacket::Handle(inner_type) } - SSH_FXP_DATA => { + SftpNum::SSH_FXP_DATA => { let inner_type = >::dec(s)?; SftpPacket::Data(inner_type) } - SSH_FXP_NAME => { + SftpNum::SSH_FXP_NAME => { let inner_type = >::dec(s)?; SftpPacket::Name(inner_type) } diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 0018e620..832fed75 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -404,8 +404,9 @@ macro_rules! sftpmessages { } } - impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> - where 'a: 'de, + + impl<'a: 'de, 'de> SSHDecode<'de> for SftpPacket<'a> + where 'de: 'a // This implies that both lifetimes are equal { fn dec(s: &mut S) -> WireResult where S: SSHSource<'de> { From 05220cc47fcbb5f58001a1423ba9d92350a3656a Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 28 Aug 2025 14:39:18 +1000 Subject: [PATCH 047/393] cargo check passing and all the code uncommented. However... I have changed the signature for methods `decode_request` and `decode_response` to avoid the dyn SSHSource which was causing a problen during calls to dec(s)? Since it does not have a known size at compile time. To do so I have included the `'de` a lifetime for the method parameter s and made the structure SftpPacket lifetime `'a` and `'de` be equal, as I did in the previous commit fab799627f96f879df874ae016182937c3bd5f1e Will this be an issue? Last, the checks to find out if the decoded packet is a response or a request in `decode_response` and `decode_request` now return WireError::PacketWrong instead of error::SSHProto.fail() or Error::bug(). This might need to change --- sftp/out/cargo-expand-sftp.rs | 54 +++++++++++++++ sftp/src/proto.rs | 126 ++++++++++++++++++++-------------- 2 files changed, 128 insertions(+), 52 deletions(-) diff --git a/sftp/out/cargo-expand-sftp.rs b/sftp/out/cargo-expand-sftp.rs index 996d330f..0c712792 100644 --- a/sftp/out/cargo-expand-sftp.rs +++ b/sftp/out/cargo-expand-sftp.rs @@ -1349,6 +1349,60 @@ mod proto { SftpPacket::Name(_) => SftpNum::from(104 as u8), } } + /// Encode a request. + /// + /// Used by a SFTP client. Does not include the length field. + pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { + if !self.sftp_num().is_request() { + return Err(WireError::PacketWrong); + } + self.sftp_num().enc(s)?; + id.0.enc(s)?; + self.enc(s) + } + /// Decode a response. + /// + /// Used by a SFTP client. Does not include the length field. + pub fn decode_response<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> + where + S: SSHSource<'de>, + 'a: 'de, + 'de: 'a, + { + let num = SftpNum::from(u8::dec(s)?); + if !num.is_response() { + return Err(WireError::PacketWrong); + } + let id = ReqId(u32::dec(s)?); + Ok((id, Self::dec(s)?)) + } + /// Decode a request. + /// + /// Used by a SFTP server. Does not include the length field. + pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> + where + S: SSHSource<'de>, + 'a: 'de, + 'de: 'a, + { + let num = SftpNum::from(u8::dec(s)?); + if !num.is_request() { + return Err(WireError::PacketWrong); + } + let id = ReqId(u32::dec(s)?); + Ok((id, Self::dec(s)?)) + } + /// Encode a response. + /// + /// Used by a SFTP server. Does not include the length field. + pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { + if !self.sftp_num().is_response() { + return Err(WireError::PacketWrong); + } + self.sftp_num().enc(s)?; + id.0.enc(s)?; + self.enc(s) + } } impl<'a> From for SftpPacket<'a> { fn from(s: InitVersionClient) -> SftpPacket<'a> { diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 832fed75..5cc74343 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -443,66 +443,89 @@ macro_rules! sftpmessages { } } -// /// Encode a request. -// /// -// /// Used by a SFTP client. Does not include the length field. -// pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> { -// if !self.sftp_num().is_request() { -// return Err(Error::bug()) -// } + /// Encode a request. + /// + /// Used by a SFTP client. Does not include the length field. + pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { + // TODO: handle the Error conversion + if !self.sftp_num().is_request() { + return Err(WireError::PacketWrong) + // return Err(Error::bug()) + // TODO: I understand that it would be a bad call of encode_response and + // therefore a bug, bug Error::bug() is not compatible with WireResult + } -// // packet type -// self.sftp_num().enc(s)?; -// // request ID -// id.0.enc(s)?; -// // contents -// self.enc(s) -// } + // packet type + self.sftp_num().enc(s)?; + // request ID + id.0.enc(s)?; + // contents + self.enc(s) + } -// /// Decode a response. -// /// -// /// Used by a SFTP client. Does not include the length field. -// pub fn decode_response(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> { -// let num = SftpNum::try_from(u8::dec(s)?)?; + /// Decode a response. + /// + /// Used by a SFTP client. Does not include the length field. + pub fn decode_response<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> + where + S: SSHSource<'de>, + 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes + 'de: 'a + { + let num = SftpNum::from(u8::dec(s)?); -// if !num.is_response() { -// return error::SSHProto.fail(); -// } + if !num.is_response() { + return Err(WireError::PacketWrong) + // return error::SSHProto.fail(); + // TODO: Not an error in the SSHProtocol rather the SFTP. + // Maybe is time to define an SftpError + } -// let id = ReqId(u32::dec(s)?); -// Ok((id, Self::dec(s))) -// } + let id = ReqId(u32::dec(s)?); + Ok((id, Self::dec(s)?)) + } -// /// Decode a request. -// /// -// /// Used by a SFTP server. Does not include the length field. -// pub fn decode_request(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> { -// let num = SftpNum::try_from(u8::dec(s)?)?; + /// Decode a request. + /// + /// Used by a SFTP server. Does not include the length field. + pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> + where + S: SSHSource<'de>, + 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes + 'de: 'a + { + let num = SftpNum::from(u8::dec(s)?); -// if !num.is_request() { -// return error::SSHProto.fail(); -// } + if !num.is_request() { + return Err(WireError::PacketWrong) + // return error::SSHProto.fail(); + // TODO: Not an error in the SSHProtocol rather the SFTP. + // Maybe is time to define an SftpError + } -// let id = ReqId(u32::dec(s)?); -// Ok((id, Self::dec(s))) -// } + let id = ReqId(u32::dec(s)?); + Ok((id, Self::dec(s)?)) + } -// /// Encode a response. -// /// -// /// Used by a SFTP server. Does not include the length field. -// pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> { -// if !self.sftp_num().is_response() { -// return Err(Error::bug()) -// } + /// Encode a response. + /// + /// Used by a SFTP server. Does not include the length field. + pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { -// // packet type -// self.sftp_num().enc(s)?; -// // request ID -// id.0.enc(s)?; -// // contents -// self.enc(s) -// } -// } + if !self.sftp_num().is_response() { + return Err(WireError::PacketWrong) + // return Err(Error::bug()) + // TODO: I understand that it would be a bad call of encode_response and + // therefore a bug, bug Error::bug() is not compatible with WireResult + } + + // packet type + self.sftp_num().enc(s)?; + // request ID + id.0.enc(s)?; + // contents + self.enc(s) + } } @@ -533,5 +556,4 @@ sftpmessages![ (102, Handle, Handle<'a>, SSH_FXP_HANDLE), (103, Data, Data<'a>, SSH_FXP_DATA), (104, Name, Name<'a>, SSH_FXP_NAME), - ]; From ef2bf9a5e4b33a772b3ba318d6720d17ebd9e2ff Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 28 Aug 2025 14:47:19 +1000 Subject: [PATCH 048/393] Fixing Avoidable warnings: - visibility for `FileHandle` - removing unused uses - removing unused local result and error --- sftp/src/proto.rs | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 5cc74343..8623de48 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,8 +1,5 @@ use num_enum::FromPrimitive; use paste::paste; -use sunset::error; -use sunset::error::Error as SunsetError; -use sunset::packets::{MessageNumber, Packet, Unknown}; use sunset::sshwire::{ BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, WireResult, @@ -15,7 +12,7 @@ use sunset_sshwire_derive::{SSHDecode, SSHEncode}; pub struct Filename<'a>(TextString<'a>); #[derive(Debug, SSHEncode, SSHDecode)] -struct FileHandle<'a>(pub BinString<'a>); +pub struct FileHandle<'a>(pub BinString<'a>); /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 const SFTP_VERSION: u32 = 3; @@ -296,25 +293,6 @@ impl<'de> SSHDecode<'de> for Attrs { } } -#[derive(Debug)] -pub enum Error { - UnknownPacket { number: u8 }, -} - -pub type Result = core::result::Result; - -// impl From for SunsetError { -// fn from(error: Error) -> SunsetError { -// SunsetError::Custom { -// msg: match error { -// Error::UnknownPacket { number } => { -// format_args!("Unknown SFTP packet: {}", number) -// } -// }, -// } -// } -// } - macro_rules! sftpmessages { ( $( ( $message_num:tt, From af2e05abe4f1c811f091eff89db4f9e9f9de0be7 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 28 Aug 2025 15:07:00 +1000 Subject: [PATCH 049/393] removing outdated TODOs in sftp/src/proto.rs --- sftp/src/proto.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 8623de48..c285cb5b 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -286,9 +286,6 @@ impl<'de> SSHDecode<'de> for Attrs { // TODO: Implement extensions // if flags & AttrsFlags::SSH_FILEXFER_ATTR_EXTENDED != 0{ - // todo!("Not implemented"); - // } - Ok(attrs) } } @@ -331,7 +328,7 @@ macro_rules! sftpmessages { fn from(sftp_num: SftpNum) -> u8 { match sftp_num { $( - SftpNum::$SSH_FXP_NAME => $message_num, // TODO: Fix this + SftpNum::$SSH_FXP_NAME => $message_num, )* _ => 0 // Other, not in the enum definition @@ -425,7 +422,6 @@ macro_rules! sftpmessages { /// /// Used by a SFTP client. Does not include the length field. pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { - // TODO: handle the Error conversion if !self.sftp_num().is_request() { return Err(WireError::PacketWrong) // return Err(Error::bug()) From 8a80477a8d7400aff11b94c786526b278285aedb Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 29 Aug 2025 11:28:12 +1000 Subject: [PATCH 050/393] Fix from comment @mkj comment on Name Fixed according to the indications in the next discussion: https://github.com/mkj/sunset/pull/29#discussion_r2306088167 Adding the expanded version for reference too Thanks --- sftp/out/cargo-expand-sftp.rs | 36 ++++++++--------------------------- sftp/src/proto.rs | 9 ++++++--- 2 files changed, 14 insertions(+), 31 deletions(-) diff --git a/sftp/out/cargo-expand-sftp.rs b/sftp/out/cargo-expand-sftp.rs index 0c712792..c1ea92d2 100644 --- a/sftp/out/cargo-expand-sftp.rs +++ b/sftp/out/cargo-expand-sftp.rs @@ -6,9 +6,6 @@ extern crate std; mod proto { use num_enum::FromPrimitive; use paste::paste; - use sunset::error; - use sunset::error::Error as SunsetError; - use sunset::packets::{MessageNumber, Packet, Unknown}; use sunset::sshwire::{ BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, WireResult, @@ -41,7 +38,7 @@ mod proto { Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) } } - struct FileHandle<'a>(pub BinString<'a>); + pub struct FileHandle<'a>(pub BinString<'a>); #[automatically_derived] impl<'a> ::core::fmt::Debug for FileHandle<'a> { #[inline] @@ -502,15 +499,18 @@ mod proto { }) } } - pub struct Name<'de>(pub Vec>); + pub struct Name<'a>(pub Vec>); #[automatically_derived] - impl<'de> ::core::fmt::Debug for Name<'de> { + impl<'a> ::core::fmt::Debug for Name<'a> { #[inline] fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Name", &&self.0) } } - impl<'de> SSHDecode<'de> for Name<'de> { + impl<'a: 'de, 'de> SSHDecode<'de> for Name<'a> + where + 'de: 'a, + { fn dec(s: &mut S) -> WireResult where S: SSHSource<'de>, @@ -523,7 +523,7 @@ mod proto { Ok(Name(names)) } } - impl SSHEncode for Name<'_> { + impl<'a> SSHEncode for Name<'a> { fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { (self.0.len() as u32).enc(s)?; for element in self.0.iter() { @@ -930,26 +930,6 @@ mod proto { Ok(attrs) } } - pub enum Error { - UnknownPacket { number: u8 }, - } - #[automatically_derived] - impl ::core::fmt::Debug for Error { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - match self { - Error::UnknownPacket { number: __self_0 } => { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "UnknownPacket", - "number", - &__self_0, - ) - } - } - } - } - pub type Result = core::result::Result; #[repr(u8)] #[allow(non_camel_case_types)] pub enum SftpNum { diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index c285cb5b..d0657250 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -90,9 +90,12 @@ pub struct NameEntry<'a> { } #[derive(Debug)] -pub struct Name<'de>(pub Vec>); +pub struct Name<'a>(pub Vec>); -impl<'de> SSHDecode<'de> for Name<'de> { +impl<'a: 'de, 'de> SSHDecode<'de> for Name<'a> +where + 'de: 'a, +{ fn dec(s: &mut S) -> WireResult where S: SSHSource<'de>, @@ -109,7 +112,7 @@ impl<'de> SSHDecode<'de> for Name<'de> { } } -impl SSHEncode for Name<'_> { +impl<'a> SSHEncode for Name<'a> { fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { (self.0.len() as u32).enc(s)?; From a3efa7ace08c72a0dcad1d9a94d3fc0fd209bd40 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Sat, 30 Aug 2025 12:25:44 +1000 Subject: [PATCH 051/393] reordering dependencies in sftp to match other workspace members --- sftp/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index b875c003..2826346c 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -4,7 +4,8 @@ version = "0.1.0" edition = "2024" [dependencies] -num_enum = {version = "0.7.4"} sunset = { version = "0.3.0", path = "../" } sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } + +num_enum = {version = "0.7.4"} paste = "1.0" \ No newline at end of file From 6192fb2ca95ea347b356968ac28a681e26045326 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Sat, 30 Aug 2025 12:29:56 +1000 Subject: [PATCH 052/393] Starting a new sftp-std demo taking the basic std demo as model It has some very basic code proving that when an sftp connection is accepted the version handshake starts SSH_FXP_INIT -> <- SSH_FXP_VERSION etc --- Cargo.lock | 24 ++++ Cargo.toml | 1 + demo/sftp/std/Cargo.toml | 38 ++++++ demo/sftp/std/debug_sftp_client.sh | 3 + demo/sftp/std/rust-toolchain.toml | 3 + demo/sftp/std/src/main.rs | 192 +++++++++++++++++++++++++++++ demo/sftp/std/tap.sh | 7 ++ 7 files changed, 268 insertions(+) create mode 100644 demo/sftp/std/Cargo.toml create mode 100755 demo/sftp/std/debug_sftp_client.sh create mode 100644 demo/sftp/std/rust-toolchain.toml create mode 100644 demo/sftp/std/src/main.rs create mode 100755 demo/sftp/std/tap.sh diff --git a/Cargo.lock b/Cargo.lock index 8dcaffa8..5def08c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2849,6 +2849,30 @@ dependencies = [ "sunset-sshwire-derive", ] +[[package]] +name = "sunset-demo-sftp-std" +version = "0.1.0" +dependencies = [ + "async-io", + "critical-section", + "embassy-executor", + "embassy-futures", + "embassy-net", + "embassy-net-tuntap", + "embassy-sync 0.7.0", + "embassy-time", + "embedded-io-async", + "env_logger", + "heapless", + "libc", + "log", + "rand", + "sha2", + "sunset", + "sunset-async", + "sunset-demo-common", +] + [[package]] name = "sunset-demo-std" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index e6a35697..8b6d8266 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "demo/picow", "demo/std", "fuzz", "sftp", + "demo/sftp/std", "stdasync", # workspace.dependencies paths are automatic ] diff --git a/demo/sftp/std/Cargo.toml b/demo/sftp/std/Cargo.toml new file mode 100644 index 00000000..da11494c --- /dev/null +++ b/demo/sftp/std/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "sunset-demo-sftp-std" +version = "0.1.0" +edition = "2021" + +[dependencies] +sunset = { workspace = true, features = ["rsa", "std"] } +sunset-async.workspace = true +sunset-demo-common.workspace = true + +# 131072 was determined empirically +embassy-executor = { version = "0.7", features = [ + "executor-thread", "arch-std", "log", "task-arena-size-131072"] } +embassy-net = { version = "0.7", features = ["tcp", "dhcpv4", "medium-ethernet"] } +embassy-net-tuntap = { version = "0.1" } +embassy-sync = { version = "0.7" } +embassy-futures = { version = "0.1" } +# embassy-time dep required to link a time driver +embassy-time = { version = "0.4", default-features=false, features = ["log", "std"] } + +log = { version = "0.4" } +# default regex feature is huge +env_logger = { version = "0.11", default-features=false, features = ["auto-color", "humantime"] } + +embedded-io-async = "0.6" +heapless = "0.8" + +# for tuntap +libc = "0.2.101" +async-io = "1.6.0" + +# using local fork +# menu = "0.3" + + +critical-section = "1.1" +rand = { version = "0.8", default-features = false, features = ["getrandom"] } +sha2 = { version = "0.10", default-features = false } diff --git a/demo/sftp/std/debug_sftp_client.sh b/demo/sftp/std/debug_sftp_client.sh new file mode 100755 index 00000000..a2e5e4ee --- /dev/null +++ b/demo/sftp/std/debug_sftp_client.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR any@192.168.69.2 \ No newline at end of file diff --git a/demo/sftp/std/rust-toolchain.toml b/demo/sftp/std/rust-toolchain.toml new file mode 100644 index 00000000..9993e936 --- /dev/null +++ b/demo/sftp/std/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = [ "rustfmt" ] diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs new file mode 100644 index 00000000..2e43fda2 --- /dev/null +++ b/demo/sftp/std/src/main.rs @@ -0,0 +1,192 @@ +use embedded_io_async::{Read, Write}; +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; + +use embassy_executor::Spawner; +use embassy_net::{Stack, StackResources, StaticConfigV4}; + +use rand::rngs::OsRng; +use rand::RngCore; + +use embassy_futures::select::select; +use embassy_net_tuntap::TunTapDevice; +use embassy_sync::channel::Channel; + +use sunset::*; +use sunset_async::{ProgressHolder, SSHServer, SunsetMutex, SunsetRawMutex}; + +pub(crate) use sunset_demo_common as demo_common; + +use demo_common::{DemoCommon, DemoServer, SSHConfig}; + +const NUM_LISTENERS: usize = 4; +// +1 for dhcp +const NUM_SOCKETS: usize = NUM_LISTENERS + 1; + +#[embassy_executor::task] +async fn net_task(mut runner: embassy_net::Runner<'static, TunTapDevice>) -> ! { + runner.run().await +} + +#[embassy_executor::task] +async fn main_task(spawner: Spawner) { + // TODO config + let opt_tap0 = "tap0"; + let ip4 = "192.168.69.2"; + let cir = 24; + + let config = Box::leak(Box::new({ + let mut config = SSHConfig::new().unwrap(); + config.set_admin_pw(Some("pw")).unwrap(); + config.console_noauth = true; + config.ip4_static = if let Ok(ip) = ip4.parse() { + Some(StaticConfigV4 { + address: embassy_net::Ipv4Cidr::new(ip, cir), + gateway: None, + dns_servers: { heapless::Vec::new() }, + }) + } else { + None + }; + SunsetMutex::new(config) + })); + + let net_cf = if let Some(ref s) = config.lock().await.ip4_static { + embassy_net::Config::ipv4_static(s.clone()) + } else { + embassy_net::Config::dhcpv4(Default::default()) + }; + info!("Net config: {net_cf:?}"); + + // Init network device + let net_device = TunTapDevice::new(opt_tap0).unwrap(); + + let seed = OsRng.next_u64(); + + // Init network stack + let res = Box::leak(Box::new(StackResources::::new())); + let (stack, runner) = embassy_net::new(net_device, net_cf, res, seed); + + // Launch network task + spawner.spawn(net_task(runner)).unwrap(); + + for _ in 0..NUM_LISTENERS { + spawner.spawn(listen(stack, config)).unwrap(); + } +} + +#[derive(Default)] +struct StdDemo; + +impl DemoServer for StdDemo { + async fn run(&self, serv: &SSHServer<'_>, mut common: DemoCommon) -> Result<()> { + let chan_pipe = Channel::::new(); + + let prog_loop = async { + loop { + let mut ph = ProgressHolder::new(); + let ev = serv.progress(&mut ph).await?; + trace!("ev {ev:?}"); + match ev { + ServEvent::SessionShell(a) => { + a.fail()?; // Not allowed in this example, kept here for compatibility + } + ServEvent::SessionExec(a) => { + a.fail()?; // Not allowed in this example, kept here for compatibility + } + ServEvent::SessionSubsystem(a) => { + match a.command()?.to_lowercase().as_str() { + "sftp" => { + info!("Starting '{}' subsystem", a.command()?); + + if let Some(ch) = common.sess.take() { + debug_assert!(ch.num() == a.channel()); + a.succeed()?; + let _ = chan_pipe.try_send(ch); + } else { + a.fail()?; + } + } + _ => { + warn!( + "request for subsystem '{}' not implemented: fail", + a.command()? + ); + a.fail()?; + } + } + } + other => common.handle_event(other)?, + }; + } + #[allow(unreachable_code)] + Ok::<_, Error>(()) + }; + + let prog_loop = async { + if let Err(e) = prog_loop.await { + warn!("Exited: {e:?}"); + } + }; + + let sftp_loop = async { + let ch = chan_pipe.receive().await; + info!("This is the sftp loop"); + // After this you are likely to receive an SSH_FXP_INIT + let mut stdio = serv.stdio(ch).await?; + + loop { + let mut b = [0u8; 200]; + let lr = stdio.read(&mut b).await?; + if lr == 0 { + break; + } + let b = &mut b[..lr]; + info!("received '{:?}'", b); + + // First packet received! [0, 0, 0, 5, 1, 0, 0, 0, 3], meaning: + // Len (u32): [0, 0, 0, 5] 5 bytes + // Type (u8): [1] SSH_FXP_INIT + // Version (u32): [0, 0, 0, 3]] version 3 + // So we need to answer 5 SSH_FXP_VERSION 3 + // [0, 0, 0, 5, 2, 0, 0, 0, 3] + let hardcoded_version_r: Vec = vec![0, 0, 0, 5, 2, 0, 0, 0, 3]; + info!("sending '{:?}'", hardcoded_version_r); + + stdio.write(hardcoded_version_r.as_slice()).await?; + } + + Ok::<_, Error>(()) + }; + + select(prog_loop, sftp_loop).await; + todo!() + } +} + +// TODO: pool_size should be NUM_LISTENERS but needs a literal +#[embassy_executor::task(pool_size = 4)] +async fn listen( + stack: Stack<'static>, + config: &'static SunsetMutex, +) -> ! { + let demo = StdDemo::default(); + demo_common::listen(stack, config, &demo).await +} + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + env_logger::builder() + .filter_level(log::LevelFilter::Trace) + // .filter_module("sunset::runner", log::LevelFilter::Info) + .filter_module("sunset::traffic", log::LevelFilter::Info) + .filter_module("sunset::encrypt", log::LevelFilter::Info) + // .filter_module("sunset::conn", log::LevelFilter::Info) + // .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) + .filter_module("async_io", log::LevelFilter::Info) + .filter_module("polling", log::LevelFilter::Info) + .format_timestamp_nanos() + .init(); + + spawner.spawn(main_task(spawner)).unwrap(); +} diff --git a/demo/sftp/std/tap.sh b/demo/sftp/std/tap.sh new file mode 100755 index 00000000..8732a0cd --- /dev/null +++ b/demo/sftp/std/tap.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# This script generates the tap device that the demo will bind the network stack +# usage `sudo ./tap.sh` + +ip tuntap add name tap0 mode tap user $SUDO_USER group $SUDO_USER +ip addr add 192.168.69.100/24 dev tap0 +ip link set tap0 up From 6950892c354e3556dde8433bf221514089a34149 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Sat, 30 Aug 2025 12:30:27 +1000 Subject: [PATCH 053/393] adding mod sftpserver as I am going to start working on it next --- sftp/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 0368d938..4c2f3ffe 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,2 +1,2 @@ mod proto; -// mod sftpserver; +mod sftpserver; From 62d32a9b941c99c5f4c70a67679c3acf2f430970 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 3 Sep 2025 13:55:40 +1000 Subject: [PATCH 054/393] Adding PathInfo SFTP Packet as it is requested by sftp clients after initialisation --- sftp/src/proto.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index d0657250..042d0623 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -61,6 +61,11 @@ pub struct Write<'a> { // Responses +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct PathInfo<'a> { + pub path: TextString<'a>, +} + #[derive(Debug, SSHEncode, SSHDecode)] pub struct Status<'a> { pub code: StatusCode, From 69765488ba6e75cdb55a4222a986b128bd487887 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 3 Sep 2025 13:57:10 +1000 Subject: [PATCH 055/393] adding some Doc comments --- sftp/src/proto.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 042d0623..0cc661fb 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -180,6 +180,7 @@ pub struct ExtPair<'a> { pub data: BinString<'a>, } +/// Files attributes to describe Files as SFTP v3 specification #[derive(Debug, Default)] pub struct Attrs { // flags: u32, defines used attributes @@ -308,6 +309,7 @@ macro_rules! sftpmessages { )* ) => { paste! { + /// Represent a subset of the SFTP packet types defined by draft-ietf-secsh-filexfer-02 #[derive(Debug, Clone, FromPrimitive, SSHEncode)] #[repr(u8)] #[allow(non_camel_case_types)] From 0e4cd3c865b794067658500fe99dca15f280babe Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 3 Sep 2025 13:58:25 +1000 Subject: [PATCH 056/393] exposing sftp entities in lib.rs --- sftp/src/lib.rs | 10 ++++++++++ sftp/src/proto.rs | 2 +- sftp/src/sftpserver.rs | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 4c2f3ffe..ae003bca 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,2 +1,12 @@ mod proto; mod sftpserver; + +pub use sftpserver::DirReply; +pub use sftpserver::ReadReply; +pub use sftpserver::Result; +pub use sftpserver::SftpServer; + +pub use proto::Attrs; +pub use proto::SFTP_VERSION; +pub use proto::SftpNum; +pub use proto::SftpPacket; diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 0cc661fb..40473041 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -15,7 +15,7 @@ pub struct Filename<'a>(TextString<'a>); pub struct FileHandle<'a>(pub BinString<'a>); /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 -const SFTP_VERSION: u32 = 3; +pub const SFTP_VERSION: u32 = 3; /// The SFTP version of the client #[derive(Debug, SSHEncode, SSHDecode)] diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 8a6ce16f..bbee31de 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -7,7 +7,7 @@ pub type Result = core::result::Result; /// Some less core operations have a Provided implementation returning /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. -trait SftpServer { +pub trait SftpServer { type Handle; // TODO flags struct From 9faa8d1fef4038cf007c06408ad8cc2f28c9f108 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 3 Sep 2025 14:03:01 +1000 Subject: [PATCH 057/393] fixing impl From for u8 captures the number in Other SftpNum --- sftp/src/proto.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 40473041..cfb63a1a 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -340,7 +340,8 @@ macro_rules! sftpmessages { $( SftpNum::$SSH_FXP_NAME => $message_num, )* - _ => 0 // Other, not in the enum definition + + SftpNum::Other(number) => number // Other, not in the enum definition } } From 3296adb512c141c122f157677af4986316e77f9c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 3 Sep 2025 20:35:49 +1000 Subject: [PATCH 058/393] Added log to sftp --- sftp/Cargo.toml | 3 ++- sftp/src/proto.rs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 2826346c..2deb566d 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -8,4 +8,5 @@ sunset = { version = "0.3.0", path = "../" } sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } num_enum = {version = "0.7.4"} -paste = "1.0" \ No newline at end of file +paste = "1.0" +log = "0.4" diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index cfb63a1a..b42c1c87 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -7,6 +7,8 @@ use sunset::sshwire::{ use sunset_sshwire_derive::{SSHDecode, SSHEncode}; +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; // TODO is utf8 enough, or does this need to be an opaque binstring? #[derive(Debug, SSHEncode, SSHDecode)] pub struct Filename<'a>(TextString<'a>); From 6fc9d397e6022168afaacb3c1b4628eb3a7ff6e1 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 3 Sep 2025 20:45:31 +1000 Subject: [PATCH 059/393] Big changes: Fixed decoding SftpPacket This changes the sftpmessge parameters to handle the three classes of SFTP Packets: Initialization, Requests and Responses. The motivation is to add a mechanism to capture the Request Ids. Since only requests and responses contain that field but they do not include in their "inner" sftp Packets definitions (See Read, Write, etc) I thought that it was a good idea keeping them out of them. To store them away from the Inner packets I decided adding extra parameters to the SftpPacket enum. To do so the parameters have been reestructured so each class of sftp packet can be expanded with the required data. As a side effect, the sftpmessages packets definition readability has been improved. Certainly this adds complexity to the sftpmessages --- sftp/out/cargo-expand-sftp.rs | 507 +++++++++++++++++++++++++++++----- sftp/src/proto.rs | 261 +++++++++++++---- 2 files changed, 635 insertions(+), 133 deletions(-) diff --git a/sftp/out/cargo-expand-sftp.rs b/sftp/out/cargo-expand-sftp.rs index c1ea92d2..0032029e 100644 --- a/sftp/out/cargo-expand-sftp.rs +++ b/sftp/out/cargo-expand-sftp.rs @@ -11,6 +11,8 @@ mod proto { WireResult, }; use sunset_sshwire_derive::{SSHDecode, SSHEncode}; + #[allow(unused_imports)] + use log::{debug, error, info, log, trace, warn}; pub struct Filename<'a>(TextString<'a>); #[automatically_derived] impl<'a> ::core::fmt::Debug for Filename<'a> { @@ -66,7 +68,7 @@ mod proto { } } /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 - const SFTP_VERSION: u32 = 3; + pub const SFTP_VERSION: u32 = 3; /// The SFTP version of the client pub struct InitVersionClient { pub version: u32, @@ -315,6 +317,41 @@ mod proto { }) } } + pub struct PathInfo<'a> { + pub path: TextString<'a>, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for PathInfo<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "PathInfo", + "path", + &&self.path, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for PathInfo<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.path, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for PathInfo<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_path = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { path: field_path }) + } + } pub struct Status<'a> { pub code: StatusCode, pub message: TextString<'a>, @@ -795,6 +832,7 @@ mod proto { }) } } + /// Files attributes to describe Files as SFTP v3 specification pub struct Attrs { pub size: Option, pub uid: Option, @@ -930,6 +968,7 @@ mod proto { Ok(attrs) } } + /// Represent a subset of the SFTP packet types defined by draft-ietf-secsh-filexfer-02 #[repr(u8)] #[allow(non_camel_case_types)] pub enum SftpNum { @@ -945,6 +984,8 @@ mod proto { SSH_FXP_READ = 5, #[sshwire(variant = "ssh_fxp_write")] SSH_FXP_WRITE = 6, + #[sshwire(variant = "ssh_fxp_realpath")] + SSH_FXP_REALPATH = 16, #[sshwire(variant = "ssh_fxp_status")] SSH_FXP_STATUS = 101, #[sshwire(variant = "ssh_fxp_handle")] @@ -981,6 +1022,9 @@ mod proto { SftpNum::SSH_FXP_WRITE => { ::core::fmt::Formatter::write_str(f, "SSH_FXP_WRITE") } + SftpNum::SSH_FXP_REALPATH => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_REALPATH") + } SftpNum::SSH_FXP_STATUS => { ::core::fmt::Formatter::write_str(f, "SSH_FXP_STATUS") } @@ -1015,6 +1059,7 @@ mod proto { SftpNum::SSH_FXP_CLOSE => SftpNum::SSH_FXP_CLOSE, SftpNum::SSH_FXP_READ => SftpNum::SSH_FXP_READ, SftpNum::SSH_FXP_WRITE => SftpNum::SSH_FXP_WRITE, + SftpNum::SSH_FXP_REALPATH => SftpNum::SSH_FXP_REALPATH, SftpNum::SSH_FXP_STATUS => SftpNum::SSH_FXP_STATUS, SftpNum::SSH_FXP_HANDLE => SftpNum::SSH_FXP_HANDLE, SftpNum::SSH_FXP_DATA => SftpNum::SSH_FXP_DATA, @@ -1035,6 +1080,7 @@ mod proto { const SSH_FXP_CLOSE__num_enum_0__: u8 = 4; const SSH_FXP_READ__num_enum_0__: u8 = 5; const SSH_FXP_WRITE__num_enum_0__: u8 = 6; + const SSH_FXP_REALPATH__num_enum_0__: u8 = 16; const SSH_FXP_STATUS__num_enum_0__: u8 = 101; const SSH_FXP_HANDLE__num_enum_0__: u8 = 102; const SSH_FXP_DATA__num_enum_0__: u8 = 103; @@ -1047,6 +1093,7 @@ mod proto { SSH_FXP_CLOSE__num_enum_0__ => Self::SSH_FXP_CLOSE, SSH_FXP_READ__num_enum_0__ => Self::SSH_FXP_READ, SSH_FXP_WRITE__num_enum_0__ => Self::SSH_FXP_WRITE, + SSH_FXP_REALPATH__num_enum_0__ => Self::SSH_FXP_REALPATH, SSH_FXP_STATUS__num_enum_0__ => Self::SSH_FXP_STATUS, SSH_FXP_HANDLE__num_enum_0__ => Self::SSH_FXP_HANDLE, SSH_FXP_DATA__num_enum_0__ => Self::SSH_FXP_DATA, @@ -1076,6 +1123,7 @@ mod proto { Self::SSH_FXP_CLOSE => {} Self::SSH_FXP_READ => {} Self::SSH_FXP_WRITE => {} + Self::SSH_FXP_REALPATH => {} Self::SSH_FXP_STATUS => {} Self::SSH_FXP_HANDLE => {} Self::SSH_FXP_DATA => {} @@ -1096,6 +1144,7 @@ mod proto { Self::SSH_FXP_CLOSE => "ssh_fxp_close", Self::SSH_FXP_READ => "ssh_fxp_read", Self::SSH_FXP_WRITE => "ssh_fxp_write", + Self::SSH_FXP_REALPATH => "ssh_fxp_realpath", Self::SSH_FXP_STATUS => "ssh_fxp_status", Self::SSH_FXP_HANDLE => "ssh_fxp_handle", Self::SSH_FXP_DATA => "ssh_fxp_data", @@ -1124,15 +1173,19 @@ mod proto { SftpNum::SSH_FXP_CLOSE => 4, SftpNum::SSH_FXP_READ => 5, SftpNum::SSH_FXP_WRITE => 6, + SftpNum::SSH_FXP_REALPATH => 16, SftpNum::SSH_FXP_STATUS => 101, SftpNum::SSH_FXP_HANDLE => 102, SftpNum::SSH_FXP_DATA => 103, SftpNum::SSH_FXP_NAME => 104, - _ => 0, + SftpNum::Other(number) => number, } } } impl SftpNum { + fn is_init(&self) -> bool { + (1..=1).contains(&(u8::from(self.clone()))) + } fn is_request(&self) -> bool { (2..=99).contains(&(u8::from(self.clone()))) } @@ -1147,14 +1200,15 @@ mod proto { pub enum SftpPacket<'a> { Init(InitVersionClient), Version(InitVersionLowest), - Open(Open<'a>), - Close(Close<'a>), - Read(Read<'a>), - Write(Write<'a>), - Status(Status<'a>), - Handle(Handle<'a>), - Data(Data<'a>), - Name(Name<'a>), + Open(ReqId, Open<'a>), + Close(ReqId, Close<'a>), + Read(ReqId, Read<'a>), + Write(ReqId, Write<'a>), + PathInfo(ReqId, PathInfo<'a>), + Status(ReqId, Status<'a>), + Handle(ReqId, Handle<'a>), + Data(ReqId, Data<'a>), + Name(ReqId, Name<'a>), } #[automatically_derived] impl<'a> ::core::fmt::Debug for SftpPacket<'a> { @@ -1175,60 +1229,76 @@ mod proto { &__self_0, ) } - SftpPacket::Open(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::Open(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Open", - &__self_0, + __self_0, + &__self_1, ) } - SftpPacket::Close(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::Close(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Close", - &__self_0, + __self_0, + &__self_1, ) } - SftpPacket::Read(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::Read(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Read", - &__self_0, + __self_0, + &__self_1, ) } - SftpPacket::Write(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::Write(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Write", - &__self_0, + __self_0, + &__self_1, ) } - SftpPacket::Status(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::PathInfo(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( + f, + "PathInfo", + __self_0, + &__self_1, + ) + } + SftpPacket::Status(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Status", - &__self_0, + __self_0, + &__self_1, ) } - SftpPacket::Handle(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::Handle(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Handle", - &__self_0, + __self_0, + &__self_1, ) } - SftpPacket::Data(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::Data(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Data", - &__self_0, + __self_0, + &__self_1, ) } - SftpPacket::Name(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::Name(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Name", - &__self_0, + __self_0, + &__self_1, ) } } @@ -1241,14 +1311,42 @@ mod proto { match self { SftpPacket::Init(p) => p.enc(s)?, SftpPacket::Version(p) => p.enc(s)?, - SftpPacket::Open(p) => p.enc(s)?, - SftpPacket::Close(p) => p.enc(s)?, - SftpPacket::Read(p) => p.enc(s)?, - SftpPacket::Write(p) => p.enc(s)?, - SftpPacket::Status(p) => p.enc(s)?, - SftpPacket::Handle(p) => p.enc(s)?, - SftpPacket::Data(p) => p.enc(s)?, - SftpPacket::Name(p) => p.enc(s)?, + SftpPacket::Open(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::Close(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::Read(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::Write(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::PathInfo(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::Status(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::Handle(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::Data(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::Name(id, p) => { + id.enc(s)?; + p.enc(s)? + } }; Ok(()) } @@ -1273,36 +1371,49 @@ mod proto { SftpPacket::Version(inner_type) } SftpNum::SSH_FXP_OPEN => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Open(inner_type) + SftpPacket::Open(req_id, inner_type) } SftpNum::SSH_FXP_CLOSE => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Close(inner_type) + SftpPacket::Close(req_id, inner_type) } SftpNum::SSH_FXP_READ => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Read(inner_type) + SftpPacket::Read(req_id, inner_type) } SftpNum::SSH_FXP_WRITE => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Write(inner_type) + SftpPacket::Write(req_id, inner_type) + } + SftpNum::SSH_FXP_REALPATH => { + let req_id = ::dec(s)?; + let inner_type = >::dec(s)?; + SftpPacket::PathInfo(req_id, inner_type) } SftpNum::SSH_FXP_STATUS => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Status(inner_type) + SftpPacket::Status(req_id, inner_type) } SftpNum::SSH_FXP_HANDLE => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Handle(inner_type) + SftpPacket::Handle(req_id, inner_type) } SftpNum::SSH_FXP_DATA => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Data(inner_type) + SftpPacket::Data(req_id, inner_type) } SftpNum::SSH_FXP_NAME => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Name(inner_type) + SftpPacket::Name(req_id, inner_type) } _ => { return Err(WireError::UnknownPacket { @@ -1319,14 +1430,15 @@ mod proto { match self { SftpPacket::Init(_) => SftpNum::from(1 as u8), SftpPacket::Version(_) => SftpNum::from(2 as u8), - SftpPacket::Open(_) => SftpNum::from(3 as u8), - SftpPacket::Close(_) => SftpNum::from(4 as u8), - SftpPacket::Read(_) => SftpNum::from(5 as u8), - SftpPacket::Write(_) => SftpNum::from(6 as u8), - SftpPacket::Status(_) => SftpNum::from(101 as u8), - SftpPacket::Handle(_) => SftpNum::from(102 as u8), - SftpPacket::Data(_) => SftpNum::from(103 as u8), - SftpPacket::Name(_) => SftpNum::from(104 as u8), + SftpPacket::Open(_, _) => SftpNum::from(3 as u8), + SftpPacket::Close(_, _) => SftpNum::from(4 as u8), + SftpPacket::Read(_, _) => SftpNum::from(5 as u8), + SftpPacket::Write(_, _) => SftpNum::from(6 as u8), + SftpPacket::PathInfo(_, _) => SftpNum::from(16 as u8), + SftpPacket::Status(_, _) => SftpNum::from(101 as u8), + SftpPacket::Handle(_, _) => SftpNum::from(102 as u8), + SftpPacket::Data(_, _) => SftpNum::from(103 as u8), + SftpPacket::Name(_, _) => SftpNum::from(104 as u8), } } /// Encode a request. @@ -1356,21 +1468,22 @@ mod proto { let id = ReqId(u32::dec(s)?); Ok((id, Self::dec(s)?)) } - /// Decode a request. + /// Decode a request. Includes Init /// - /// Used by a SFTP server. Does not include the length field. - pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> + /// Used by a SFTP server. Does not include the length field. If the request does not have id (Initialisation) + pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(Option, Self)> where S: SSHSource<'de>, 'a: 'de, 'de: 'a, { let num = SftpNum::from(u8::dec(s)?); - if !num.is_request() { + if (!num.is_request() && !num.is_init()) { return Err(WireError::PacketWrong); } - let id = ReqId(u32::dec(s)?); - Ok((id, Self::dec(s)?)) + let maybe_id = if num.is_init() { None } else { Some(ReqId(u32::dec(s)?)) }; + let sftp_packet = Self::dec(s)?; + Ok((maybe_id, sftp_packet)) } /// Encode a response. /// @@ -1394,44 +1507,294 @@ mod proto { SftpPacket::Version(s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Open<'a>) -> SftpPacket<'a> { - SftpPacket::Open(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_open", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Open(ReqId(0), s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Close<'a>) -> SftpPacket<'a> { - SftpPacket::Close(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_close", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Close(ReqId(0), s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Read<'a>) -> SftpPacket<'a> { - SftpPacket::Read(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_read", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Read(ReqId(0), s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Write<'a>) -> SftpPacket<'a> { - SftpPacket::Write(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_write", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Write(ReqId(0), s) + } + } + /// **Warning**: No Sequence Id can be infered from a Packet Type + impl<'a> From> for SftpPacket<'a> { + fn from(s: PathInfo<'a>) -> SftpPacket<'a> { + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_realpath", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::PathInfo(ReqId(0), s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Status<'a>) -> SftpPacket<'a> { - SftpPacket::Status(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_status", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Status(ReqId(0), s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Handle<'a>) -> SftpPacket<'a> { - SftpPacket::Handle(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_handle", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Handle(ReqId(0), s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Data<'a>) -> SftpPacket<'a> { - SftpPacket::Data(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_data", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Data(ReqId(0), s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Name<'a>) -> SftpPacket<'a> { - SftpPacket::Name(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_name", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Name(ReqId(0), s) } } } +mod sftpserver { + use crate::proto::{Attrs, StatusCode}; + use core::marker::PhantomData; + pub type Result = core::result::Result; + /// All trait functions are optional in the SFTP protocol. + /// Some less core operations have a Provided implementation returning + /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, + /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. + pub trait SftpServer { + type Handle; + async fn open(filename: &str, flags: u32, attrs: &Attrs) -> Result; + /// Close either a file or directory handle + async fn close(handle: &Self::Handle) -> Result<()>; + async fn read( + handle: &Self::Handle, + offset: u64, + reply: &mut ReadReply, + ) -> Result<()>; + async fn write(handle: &Self::Handle, offset: u64, buf: &[u8]) -> Result<()>; + async fn opendir(dir: &str) -> Result; + async fn readdir(handle: &Self::Handle, reply: &mut DirReply) -> Result<()>; + } + pub struct ReadReply<'g, 'a> { + chan: ChanOut<'g, 'a>, + } + impl<'g, 'a> ReadReply<'g, 'a> { + pub async fn reply(self, data: &[u8]) {} + } + pub struct DirReply<'g, 'a> { + chan: ChanOut<'g, 'a>, + } + impl<'g, 'a> DirReply<'g, 'a> { + pub async fn reply(self, data: &[u8]) {} + } + pub struct ChanOut<'g, 'a> { + _phantom_g: PhantomData<&'g ()>, + _phantom_a: PhantomData<&'a ()>, + } +} +pub use sftpserver::DirReply; +pub use sftpserver::ReadReply; +pub use sftpserver::Result; +pub use sftpserver::SftpServer; +pub use proto::Attrs; +pub use proto::SFTP_VERSION; +pub use proto::SftpNum; +pub use proto::SftpPacket; diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index b42c1c87..4fb6f832 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -303,12 +303,30 @@ impl<'de> SSHDecode<'de> for Attrs { macro_rules! sftpmessages { ( - $( ( $message_num:tt, - $SpecificPacketVariant:ident, - $SpecificPacketType:ty, - $SSH_FXP_NAME:ident - ), - )* + init: { + $( ( $init_message_num:tt, + $init_packet_variant:ident, + $init_packet_type:ty, + $init_ssh_fxp_name:literal + ), + )* + }, + request: { + $( ( $request_message_num:tt, + $request_packet_variant:ident, + $request_packet_type:ty, + $request_ssh_fxp_name:literal + ), + )* + }, + response: { + $( ( $response_message_num:tt, + $response_packet_variant:ident, + $response_packet_type:ty, + $response_ssh_fxp_name:literal + ), + )* + }, ) => { paste! { /// Represent a subset of the SFTP packet types defined by draft-ietf-secsh-filexfer-02 @@ -316,14 +334,24 @@ macro_rules! sftpmessages { #[repr(u8)] #[allow(non_camel_case_types)] pub enum SftpNum { - // SSH_FXP_OPEN = 3, - $( - #[sshwire(variant = $SSH_FXP_NAME:lower)] - $SSH_FXP_NAME = $message_num, - )* - #[sshwire(unknown)] - #[num_enum(catch_all)] - Other(u8), + $( + #[sshwire(variant = $init_ssh_fxp_name)] + [<$init_ssh_fxp_name:upper>] = $init_message_num, + )* + + $( + #[sshwire(variant = $request_ssh_fxp_name)] + [<$request_ssh_fxp_name:upper>] = $request_message_num, + )* + + $( + #[sshwire(variant = $response_ssh_fxp_name)] + [<$response_ssh_fxp_name:upper>] = $response_message_num, + )* + + #[sshwire(unknown)] + #[num_enum(catch_all)] + Other(u8), } } // paste @@ -335,12 +363,18 @@ macro_rules! sftpmessages { Ok(SftpNum::from(u8::dec(s)?)) } } - + paste!{ impl From for u8{ fn from(sftp_num: SftpNum) -> u8 { match sftp_num { - $( - SftpNum::$SSH_FXP_NAME => $message_num, + $( + SftpNum::[<$init_ssh_fxp_name:upper>] => $init_message_num, + )* + $( + SftpNum::[<$request_ssh_fxp_name:upper>] => $request_message_num, + )* + $( + SftpNum::[<$response_ssh_fxp_name:upper>] => $response_message_num, )* SftpNum::Other(number) => number // Other, not in the enum definition @@ -350,7 +384,13 @@ macro_rules! sftpmessages { } + } //paste + impl SftpNum { + fn is_init(&self) -> bool { + (1..=1).contains(&(u8::from(self.clone()))) + } + fn is_request(&self) -> bool { // TODO SSH_FXP_EXTENDED (2..=99).contains(&(u8::from(self.clone()))) @@ -362,18 +402,26 @@ macro_rules! sftpmessages { } } + /// Top level SSH packet enum /// /// It helps identifying the SFTP Packet type and handling it accordingly /// This is done using the SFTP field type #[derive(Debug)] pub enum SftpPacket<'a> { - // eg Open(Open<'a>), - $( - $SpecificPacketVariant($SpecificPacketType), - )* + $( + $init_packet_variant($init_packet_type), + )* + $( + $request_packet_variant(ReqId, $request_packet_type), + )* + $( + $response_packet_variant(ReqId, $response_packet_type), + )* + } + impl SSHEncode for SftpPacket<'_> { fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { let t = u8::from(self.sftp_num()); @@ -383,7 +431,19 @@ macro_rules! sftpmessages { // SftpPacket::KexInit(p) => { // ... $( - SftpPacket::$SpecificPacketVariant(p) => { + SftpPacket::$init_packet_variant(p) => { + p.enc(s)? + } + )* + $( + SftpPacket::$request_packet_variant(id, p) => { + id.enc(s)?; + p.enc(s)? + } + )* + $( + SftpPacket::$response_packet_variant(id, p) => { + id.enc(s)?; p.enc(s)? } )* @@ -392,6 +452,8 @@ macro_rules! sftpmessages { } } + paste!{ + impl<'a: 'de, 'de> SSHDecode<'de> for SftpPacket<'a> where 'de: 'a // This implies that both lifetimes are equal @@ -404,9 +466,27 @@ macro_rules! sftpmessages { let decoded_packet = match packet_type { $( - SftpNum::$SSH_FXP_NAME => { - let inner_type = <$SpecificPacketType>::dec(s)?; - SftpPacket::$SpecificPacketVariant(inner_type) + SftpNum::[<$init_ssh_fxp_name:upper>] => { + + let inner_type = <$init_packet_type>::dec(s)?; + SftpPacket::$init_packet_variant(inner_type) + + }, + )* + $( + SftpNum::[<$request_ssh_fxp_name:upper>] => { + let req_id = ::dec(s)?; + let inner_type = <$request_packet_type>::dec(s)?; + SftpPacket::$request_packet_variant(req_id,inner_type) + + }, + )* + $( + SftpNum::[<$response_ssh_fxp_name:upper>] => { + let req_id = ::dec(s)?; + let inner_type = <$response_packet_type>::dec(s)?; + SftpPacket::$response_packet_variant(req_id,inner_type) + }, )* _ => return Err(WireError::UnknownPacket { number: packet_type_number }) @@ -414,6 +494,7 @@ macro_rules! sftpmessages { Ok(decoded_packet) } } + } // paste impl<'a> SftpPacket<'a> { /// Maps `SpecificPacketVariant` to `message_num` @@ -423,9 +504,21 @@ macro_rules! sftpmessages { // SftpPacket::Open(_) => { // .. $( - SftpPacket::$SpecificPacketVariant(_) => { + SftpPacket::$init_packet_variant(_) => { + + SftpNum::from($init_message_num as u8) + } + )* + $( + SftpPacket::$request_packet_variant(_,_) => { - SftpNum::from($message_num as u8) + SftpNum::from($request_message_num as u8) + } + )* + $( + SftpPacket::$response_packet_variant(_,_) => { + + SftpNum::from($response_message_num as u8) } )* } @@ -472,26 +565,47 @@ macro_rules! sftpmessages { Ok((id, Self::dec(s)?)) } - /// Decode a request. + + /// Decode a request. Includes Init /// - /// Used by a SFTP server. Does not include the length field. - pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> + /// Used by a SFTP server. Does not include the length field. If the request does not have id (Initialisation) + pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(Self)> where S: SSHSource<'de>, 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes 'de: 'a { - let num = SftpNum::from(u8::dec(s)?); - if !num.is_request() { - return Err(WireError::PacketWrong) - // return error::SSHProto.fail(); - // TODO: Not an error in the SSHProtocol rather the SFTP. - // Maybe is time to define an SftpError - } + // let sftp_packet = Self::dec(s)?; + // if (!sftp_packet.sftp_num().is_request() + // && !sftp_packet.sftp_num().is_init()) + // {return Err(WireError::PacketWrong)} - let id = ReqId(u32::dec(s)?); - Ok((id, Self::dec(s)?)) + // let maybe_id = if sftp_packet.sftp_num().is_init(){ + // None + // } else{ + + // Some(ReqId(u32::dec(s)?)) + // }; + + // let num = SftpNum::from(u8::dec(s)?); + + // if (!num.is_request() + // && !num.is_init()) + // {return Err(WireError::PacketWrong)} + + + // let maybe_id = if num.is_init(){ + // None + // } else{ + + // Some(ReqId(u32::dec(s)?)) + // }; + + let inner_sftp_packet = Self::dec(s)? ; + // let sftp_packet = Self::dec(s)?; + + Ok( inner_sftp_packet) } /// Encode a response. @@ -516,31 +630,56 @@ macro_rules! sftpmessages { } -$( -impl<'a> From<$SpecificPacketType> for SftpPacket<'a> { - fn from(s: $SpecificPacketType) -> SftpPacket<'a> { - SftpPacket::$SpecificPacketVariant(s) //find me - } -} -)* + $( + impl<'a> From<$init_packet_type> for SftpPacket<'a> { + fn from(s: $init_packet_type) -> SftpPacket<'a> { + SftpPacket::$init_packet_variant(s) //find me + } + } + )* + $( + /// **Warning**: No Sequence Id can be infered from a Packet Type + impl<'a> From<$request_packet_type> for SftpPacket<'a> { + fn from(s: $request_packet_type) -> SftpPacket<'a> { + warn!("Casting from {:?} to SftpPacket cannot set Request Id",$request_ssh_fxp_name); + SftpPacket::$request_packet_variant(ReqId(0), s) + } + } + )* + $( + /// **Warning**: No Sequence Id can be infered from a Packet Type + impl<'a> From<$response_packet_type> for SftpPacket<'a> { + fn from(s: $response_packet_type) -> SftpPacket<'a> { + warn!("Casting from {:?} to SftpPacket cannot set Request Id",$response_ssh_fxp_name); + SftpPacket::$response_packet_variant(ReqId(0), s) + } + } + )* + + }; // main macro + +} // sftpmessages macro -} } // macro +sftpmessages! [ -sftpmessages![ + init:{ + (1, Init, InitVersionClient, "ssh_fxp_init"), + (2, Version, InitVersionLowest, "ssh_fxp_version"), + }, -// Message number ranges are also used by Sftpnum::is_request and is_response. + request: { + (3, Open, Open<'a>, "ssh_fxp_open"), + (4, Close, Close<'a>, "ssh_fxp_close"), + (5, Read, Read<'a>, "ssh_fxp_read"), + (6, Write, Write<'a>, "ssh_fxp_write"), + (16, PathInfo, PathInfo<'a>, "ssh_fxp_realpath"), + }, -(1, Init, InitVersionClient, SSH_FXP_INIT), - (2, Version, InitVersionLowest, SSH_FXP_VERSION), - // Requests - (3, Open, Open<'a>, SSH_FXP_OPEN), - (4, Close, Close<'a>, SSH_FXP_CLOSE), - (5, Read, Read<'a>, SSH_FXP_READ), - (6, Write, Write<'a>, SSH_FXP_WRITE), + response: { + (101, Status, Status<'a>, "ssh_fxp_status"), + (102, Handle, Handle<'a>, "ssh_fxp_handle"), + (103, Data, Data<'a>, "ssh_fxp_data"), + (104, Name, Name<'a>, "ssh_fxp_name"), - // Responses - (101, Status, Status<'a>, SSH_FXP_STATUS), - (102, Handle, Handle<'a>, SSH_FXP_HANDLE), - (103, Data, Data<'a>, SSH_FXP_DATA), - (104, Name, Name<'a>, SSH_FXP_NAME), + }, ]; From d5304f1aff842fec2e5d5e9a66db90163bd4a79c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 4 Sep 2025 15:58:17 +1000 Subject: [PATCH 060/393] adding from str for Filename --- sftp/src/proto.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 4fb6f832..1ac36282 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -13,6 +13,12 @@ use log::{debug, error, info, log, trace, warn}; #[derive(Debug, SSHEncode, SSHDecode)] pub struct Filename<'a>(TextString<'a>); +impl<'a> From<&'a str> for Filename<'a> { + fn from(s: &'a str) -> Self { + Filename(TextString(s.as_bytes())) + } +} + #[derive(Debug, SSHEncode, SSHDecode)] pub struct FileHandle<'a>(pub BinString<'a>); From 092b353c347bb6e634e12fa2c08db5ee3b76fa6b Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 4 Sep 2025 16:03:37 +1000 Subject: [PATCH 061/393] changing ranges for is_init, is_request, is_response --- sftp/src/proto.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 1ac36282..fea11f9d 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -399,12 +399,13 @@ macro_rules! sftpmessages { fn is_request(&self) -> bool { // TODO SSH_FXP_EXTENDED - (2..=99).contains(&(u8::from(self.clone()))) + (3..=20).contains(&(u8::from(self.clone()))) } fn is_response(&self) -> bool { // TODO SSH_FXP_EXTENDED_REPLY - (100..=199).contains(&(u8::from(self.clone()))) + (100..=105).contains(&(u8::from(self.clone()))) + ||(2..=2).contains(&(u8::from(self.clone()))) } } From e206008ab909966fa2cb1b75867aa6acb0008e72 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 4 Sep 2025 16:05:51 +1000 Subject: [PATCH 062/393] fixing decode_request --- sftp/src/proto.rs | 43 +++++++++++++------------------------------ 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index fea11f9d..9d784758 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -573,9 +573,11 @@ macro_rules! sftpmessages { } - /// Decode a request. Includes Init + /// Decode a request. Includes Initialization packets /// - /// Used by a SFTP server. Does not include the length field. If the request does not have id (Initialisation) + /// Used by a SFTP server. Does not include the length field. + /// + /// It will fail if the received packet is a response pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(Self)> where S: SSHSource<'de>, @@ -583,41 +585,22 @@ macro_rules! sftpmessages { 'de: 'a { - // let sftp_packet = Self::dec(s)?; - // if (!sftp_packet.sftp_num().is_request() - // && !sftp_packet.sftp_num().is_init()) - // {return Err(WireError::PacketWrong)} - - // let maybe_id = if sftp_packet.sftp_num().is_init(){ - // None - // } else{ - - // Some(ReqId(u32::dec(s)?)) - // }; - - // let num = SftpNum::from(u8::dec(s)?); - - // if (!num.is_request() - // && !num.is_init()) - // {return Err(WireError::PacketWrong)} + let sftp_packet = Self::dec(s)? ; + if (!sftp_packet.sftp_num().is_request() + && !sftp_packet.sftp_num().is_init()) + { + return Err(WireError::PacketWrong) + } - // let maybe_id = if num.is_init(){ - // None - // } else{ - - // Some(ReqId(u32::dec(s)?)) - // }; - - let inner_sftp_packet = Self::dec(s)? ; - // let sftp_packet = Self::dec(s)?; - - Ok( inner_sftp_packet) + Ok( sftp_packet) } /// Encode a response. /// /// Used by a SFTP server. Does not include the length field. + /// + /// Fails if the encoded SFTP Packet is not a response pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { if !self.sftp_num().is_response() { From d20757ce29c7e39b63adca5aa6a46a17e1e68264 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 4 Sep 2025 16:10:47 +1000 Subject: [PATCH 063/393] Fixing the exposed items in SFTP library. More changes will come --- sftp/src/lib.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index ae003bca..1db3d32c 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -2,11 +2,14 @@ mod proto; mod sftpserver; pub use sftpserver::DirReply; +pub use sftpserver::ItemHandle; pub use sftpserver::ReadReply; -pub use sftpserver::Result; +pub use sftpserver::SftpHandler; +pub use sftpserver::SftpResult; pub use sftpserver::SftpServer; pub use proto::Attrs; -pub use proto::SFTP_VERSION; -pub use proto::SftpNum; -pub use proto::SftpPacket; +pub use proto::Filename; +pub use proto::Name; +pub use proto::NameEntry; +pub use proto::PathInfo; From d298642ce6ec9d092bada45a254420df025c75ee Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 4 Sep 2025 16:12:07 +1000 Subject: [PATCH 064/393] added sunset-sftp to sftp demo --- demo/sftp/std/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/sftp/std/Cargo.toml b/demo/sftp/std/Cargo.toml index da11494c..0c2a8205 100644 --- a/demo/sftp/std/Cargo.toml +++ b/demo/sftp/std/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" sunset = { workspace = true, features = ["rsa", "std"] } sunset-async.workspace = true sunset-demo-common.workspace = true +sunset-sftp = { version = "0.1.0", path = "../../../sftp" } # 131072 was determined empirically embassy-executor = { version = "0.7", features = [ From c734a1e05b716a4a3b847a85ae315b9fc310fbb7 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 4 Sep 2025 16:13:08 +1000 Subject: [PATCH 065/393] adding log dep to sunset-sftp --- Cargo.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 5def08c7..e709aa23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2871,6 +2871,7 @@ dependencies = [ "sunset", "sunset-async", "sunset-demo-common", + "sunset-sftp", ] [[package]] @@ -2915,6 +2916,7 @@ dependencies = [ name = "sunset-sftp" version = "0.1.0" dependencies = [ + "log", "num_enum 0.7.4", "paste", "sunset", From 6f64f881cd03c1f3cdfa1925cba4b00161ce52f6 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 5 Sep 2025 09:58:52 +1000 Subject: [PATCH 066/393] WIP: Adding demosftpserver.rs This will be the local implementation to serve the requests from the client. For now just structure and a mock realpath --- demo/sftp/std/src/demosftpserver.rs | 72 +++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 demo/sftp/std/src/demosftpserver.rs diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs new file mode 100644 index 00000000..44318953 --- /dev/null +++ b/demo/sftp/std/src/demosftpserver.rs @@ -0,0 +1,72 @@ +use std::str::FromStr; + +use sunset::TextString; +use sunset_sftp::{ + Attrs, DirReply, Filename, ItemHandle, Name, NameEntry, ReadReply, SftpResult, + SftpServer, +}; + +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; + +pub struct DemoSftpServer {} + +impl SftpServer for DemoSftpServer { + type Handle = ItemHandle; + + async fn open( + filename: &str, + flags: u32, + attrs: &Attrs, + ) -> SftpResult { + todo!() + } + + async fn close(handle: &Self::Handle) -> SftpResult<()> { + todo!() + } + + async fn read( + handle: &Self::Handle, + offset: u64, + reply: &mut ReadReply<'_, '_>, + ) -> SftpResult<()> { + todo!() + } + + async fn write( + handle: &Self::Handle, + offset: u64, + buf: &[u8], + ) -> SftpResult<()> { + todo!() + } + + async fn opendir(dir: &str) -> SftpResult { + todo!() + } + + async fn readdir( + handle: &Self::Handle, + reply: &mut DirReply<'_, '_>, + ) -> SftpResult<()> { + todo!() + } + + async fn realpath(dir: &str) -> SftpResult> { + debug!("finding path for: {:?}", dir); + Ok(Name(vec![NameEntry { + filename: Filename::from("/root/just/kidding"), + _longname: Filename::from(""), + attrs: Attrs { + size: None, + uid: None, + gid: None, + permissions: None, + atime: None, + mtime: None, + ext_count: None, + }, + }])) + } +} From 619a18495bf33ae9edbed0795f008bc8c9a08378 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 5 Sep 2025 10:00:01 +1000 Subject: [PATCH 067/393] adding the module demosftpserver to the demo. Reordering uses --- demo/sftp/std/src/main.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 2e43fda2..bf1b5536 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -1,6 +1,14 @@ +use sunset::*; +use sunset_async::{ProgressHolder, SSHServer, SunsetMutex, SunsetRawMutex}; +use sunset_sftp::SftpHandler; + +pub(crate) use sunset_demo_common as demo_common; + +use demo_common::{DemoCommon, DemoServer, SSHConfig}; + +use crate::demosftpserver::DemoSftpServer; + use embedded_io_async::{Read, Write}; -#[allow(unused_imports)] -use log::{debug, error, info, log, trace, warn}; use embassy_executor::Spawner; use embassy_net::{Stack, StackResources, StaticConfigV4}; @@ -12,12 +20,10 @@ use embassy_futures::select::select; use embassy_net_tuntap::TunTapDevice; use embassy_sync::channel::Channel; -use sunset::*; -use sunset_async::{ProgressHolder, SSHServer, SunsetMutex, SunsetRawMutex}; - -pub(crate) use sunset_demo_common as demo_common; +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; -use demo_common::{DemoCommon, DemoServer, SSHConfig}; +mod demosftpserver; const NUM_LISTENERS: usize = 4; // +1 for dhcp From fd80127c00a8617769a187f69efe1f7b6fd54d26 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 5 Sep 2025 10:00:57 +1000 Subject: [PATCH 068/393] Reordering uses to clarify internal and external dependencies --- sftp/src/proto.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 9d784758..6fff1bda 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,14 +1,13 @@ -use num_enum::FromPrimitive; -use paste::paste; use sunset::sshwire::{ BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, WireResult, }; - use sunset_sshwire_derive::{SSHDecode, SSHEncode}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; +use num_enum::FromPrimitive; +use paste::paste; // TODO is utf8 enough, or does this need to be an opaque binstring? #[derive(Debug, SSHEncode, SSHDecode)] pub struct Filename<'a>(TextString<'a>); @@ -593,7 +592,7 @@ macro_rules! sftpmessages { return Err(WireError::PacketWrong) } - Ok( sftp_packet) + Ok(sftp_packet) } /// Encode a response. From f6928848af3cbf581447c1a29c12d9788d5a32e3 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 5 Sep 2025 10:01:52 +1000 Subject: [PATCH 069/393] tyding up dependencies --- sftp/src/sftpserver.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index bbee31de..b250ce6a 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,7 +1,8 @@ -use crate::proto::{Attrs, StatusCode}; +use crate::proto::{Attrs, Name, StatusCode}; + use core::marker::PhantomData; -pub type Result = core::result::Result; +pub type SftpResult = core::result::Result; /// All trait functions are optional in the SFTP protocol. /// Some less core operations have a Provided implementation returning From dc022c80996faad49dce2ec4a9cf02c942e565d9 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 5 Sep 2025 10:05:50 +1000 Subject: [PATCH 070/393] adding trait associated function realpath and struct ItemHandle Still deciding on how to use ItemHandle --- sftp/src/sftpserver.rs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index b250ce6a..f69342c3 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -12,23 +12,30 @@ pub trait SftpServer { type Handle; // TODO flags struct - async fn open(filename: &str, flags: u32, attrs: &Attrs) - -> Result; + async fn open( + filename: &str, + flags: u32, + attrs: &Attrs, + ) -> SftpResult; /// Close either a file or directory handle - async fn close(handle: &Self::Handle) -> Result<()>; + async fn close(handle: &Self::Handle) -> SftpResult<()>; async fn read( handle: &Self::Handle, offset: u64, reply: &mut ReadReply, - ) -> Result<()>; + ) -> SftpResult<()>; - async fn write(handle: &Self::Handle, offset: u64, buf: &[u8]) -> Result<()>; + async fn write(handle: &Self::Handle, offset: u64, buf: &[u8]) + -> SftpResult<()>; - async fn opendir(dir: &str) -> Result; + async fn opendir(dir: &str) -> SftpResult; - async fn readdir(handle: &Self::Handle, reply: &mut DirReply) -> Result<()>; + async fn readdir(handle: &Self::Handle, reply: &mut DirReply) -> SftpResult<()>; + + /// Provides the real path of the directory specified + async fn realpath(dir: &str) -> SftpResult>; } pub struct ReadReply<'g, 'a> { @@ -52,3 +59,8 @@ pub struct ChanOut<'g, 'a> { _phantom_g: PhantomData<&'g ()>, _phantom_a: PhantomData<&'a ()>, } + +#[derive(Debug)] +pub struct ItemHandle { + client_opaque_handle: String, +} From 0b69ff711f88a4e86e01e31e8a7f4a61ca94af31 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 5 Sep 2025 10:17:42 +1000 Subject: [PATCH 071/393] WIP Added sftphandle.rs This is the "traffic coordinator" for SFTP. - Handles incoming packets deserialisation - Handle the initialisation - Calls SftpServer trait to handle client requests - Handles incoming packets serialisation The current implementation tries limiting its dependencies to the minimum. Therefore it does not handle the SSH Channels itself but only buffers. This also allow the user to make decisions on the buffers. the sftp demo has been updated to use this, delegating the sftp details to the sftphandle and demosftpserver --- demo/sftp/std/src/main.rs | 60 ++++++------ sftp/src/lib.rs | 4 +- sftp/src/sftphandle.rs | 198 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+), 31 deletions(-) create mode 100644 sftp/src/sftphandle.rs diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index bf1b5536..85b7021d 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -88,7 +88,7 @@ impl DemoServer for StdDemo { async fn run(&self, serv: &SSHServer<'_>, mut common: DemoCommon) -> Result<()> { let chan_pipe = Channel::::new(); - let prog_loop = async { + let prog_loop_inner = async { loop { let mut ph = ProgressHolder::new(); let ev = serv.progress(&mut ph).await?; @@ -130,43 +130,41 @@ impl DemoServer for StdDemo { }; let prog_loop = async { - if let Err(e) = prog_loop.await { - warn!("Exited: {e:?}"); + info!("prog_loop started"); + if let Err(e) = prog_loop_inner.await { + warn!("Prog Loop Exited: {e:?}"); + return Err(e); } + Ok(()) }; let sftp_loop = async { let ch = chan_pipe.receive().await; - info!("This is the sftp loop"); - // After this you are likely to receive an SSH_FXP_INIT + info!("SFTP loop has received a channel handle"); + let mut stdio = serv.stdio(ch).await?; + let mut buffer_in = [0u8; 1000]; + let mut buffer_out = [0u8; 1000]; + + let mut sftp_handler = + SftpHandler::::new(&buffer_in, &mut buffer_out); loop { - let mut b = [0u8; 200]; - let lr = stdio.read(&mut b).await?; - if lr == 0 { - break; - } - let b = &mut b[..lr]; - info!("received '{:?}'", b); - - // First packet received! [0, 0, 0, 5, 1, 0, 0, 0, 3], meaning: - // Len (u32): [0, 0, 0, 5] 5 bytes - // Type (u8): [1] SSH_FXP_INIT - // Version (u32): [0, 0, 0, 3]] version 3 - // So we need to answer 5 SSH_FXP_VERSION 3 - // [0, 0, 0, 5, 2, 0, 0, 0, 3] - let hardcoded_version_r: Vec = vec![0, 0, 0, 5, 2, 0, 0, 0, 3]; - info!("sending '{:?}'", hardcoded_version_r); - - stdio.write(hardcoded_version_r.as_slice()).await?; - } + let lr = stdio.read(&mut buffer_in).await?; + debug!("SFTP <---- received: {:?}", &buffer_in[0..lr]); + + let lw = + sftp_handler.process(&buffer_in[0..lr], &mut buffer_out).await?; + stdio.write(&mut buffer_out[0..lw]).await?; + debug!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); + } Ok::<_, Error>(()) }; - select(prog_loop, sftp_loop).await; - todo!() + let selected = select(prog_loop, sftp_loop).await; + error!("Selected finished: {:?}", selected); + todo!("Loop terminated: {:?}", selected) } } @@ -183,14 +181,16 @@ async fn listen( #[embassy_executor::main] async fn main(spawner: Spawner) { env_logger::builder() - .filter_level(log::LevelFilter::Trace) - // .filter_module("sunset::runner", log::LevelFilter::Info) + .filter_level(log::LevelFilter::Debug) + .filter_module("sunset::runner", log::LevelFilter::Info) .filter_module("sunset::traffic", log::LevelFilter::Info) .filter_module("sunset::encrypt", log::LevelFilter::Info) - // .filter_module("sunset::conn", log::LevelFilter::Info) - // .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) + .filter_module("sunset::conn", log::LevelFilter::Info) + .filter_module("sunset::kex", log::LevelFilter::Info) + .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) .filter_module("async_io", log::LevelFilter::Info) .filter_module("polling", log::LevelFilter::Info) + .filter_module("embassy_net", log::LevelFilter::Info) .format_timestamp_nanos() .init(); diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 1db3d32c..625fe3df 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,13 +1,15 @@ mod proto; +mod sftphandle; mod sftpserver; pub use sftpserver::DirReply; pub use sftpserver::ItemHandle; pub use sftpserver::ReadReply; -pub use sftpserver::SftpHandler; pub use sftpserver::SftpResult; pub use sftpserver::SftpServer; +pub use sftphandle::SftpHandler; + pub use proto::Attrs; pub use proto::Filename; pub use proto::Name; diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs new file mode 100644 index 00000000..a83086bd --- /dev/null +++ b/sftp/src/sftphandle.rs @@ -0,0 +1,198 @@ +use crate::proto::{ + InitVersionLowest, ReqId, SFTP_VERSION, SftpPacket, Status, StatusCode, +}; +use crate::sftpserver::{ItemHandle, SftpServer}; + +use sunset::sshwire::{SSHDecode, SSHSink, SSHSource, WireError, WireResult}; + +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; +use std::marker::PhantomData; + +#[derive(Default, Debug)] +pub struct SftpSource<'de> { + pub buffer: &'de [u8], + pub index: usize, +} + +impl<'de> SSHSource<'de> for SftpSource<'de> { + fn take(&mut self, len: usize) -> sunset::sshwire::WireResult<&'de [u8]> { + if len + self.index > self.buffer.len() { + return Err(WireError::NoRoom); + } + let original_index = self.index; + let slice = &self.buffer[self.index..self.index + len]; + self.index += len; + trace!( + "slice returned: {:?}. original index {:?}, new index: {:?}", + slice, original_index, self.index + ); + Ok(slice) + } + + fn remaining(&self) -> usize { + self.buffer.len() - self.index + } + + fn ctx(&mut self) -> &mut sunset::packets::ParseContext { + todo!( + "I don't know what to do with the ctx, since sftp does not have context" + ); + } +} + +// // This implementation is an extension of the SSHSource interface. +// impl<'de> SftpSource<'de> { +// /// Rewinds the index back to the initial byte +// /// +// /// In case of an error deserialising the SSHSource it allows reprocesing the buffer from start +// pub fn rewind(&mut self) -> () { +// self.index = 0; +// } +// } + +#[derive(Default)] +pub struct SftpSink<'g> { + pub buffer: &'g mut [u8], + index: usize, +} + +impl<'g> SftpSink<'g> { + const LENG_FIELD_LEN: usize = 4; // TODO: Move it to a better location + + pub fn new(s: &'g mut [u8]) -> Self { + SftpSink { buffer: s, index: 0 } + // SftpSink { buffer: s, index: SftpSink::LENG_FIELD_LEN } + } + + /// Finalise the buffer by prepending the payload size and returning + /// + /// Returns the final index in the buffer as a reference for the space used + pub fn finalise(&mut self) -> usize { + if self.index <= SftpSink::LENG_FIELD_LEN { + warn!("SftpSink trying to terminate it before pushing data"); + return 0; + } // size is 0 + let used_size = (self.index - 4) as u32; + + used_size + .to_be_bytes() + .iter() + .enumerate() + .for_each(|(i, v)| self.buffer[i] = *v); + + self.index + } +} + +impl<'g> SSHSink for SftpSink<'g> { + fn push(&mut self, v: &[u8]) -> sunset::sshwire::WireResult<()> { + if v.len() + self.index > self.buffer.len() { + return Err(WireError::NoRoom); + } + v.iter().for_each(|val| { + self.buffer[self.index] = *val; + self.index += 1; + }); + {} + Ok(()) + } +} + +#[derive(Debug)] +pub struct SftpHandler +where + T: SftpServer, +{ + server_type: PhantomData, + handle_list: Vec, + initialized: bool, +} + +impl SftpHandler +where + T: SftpServer, +{ + pub fn new(buffer_in: &[u8], buffer_out: &mut [u8]) -> Self { + SftpHandler { + server_type: PhantomData, + handle_list: vec![], + initialized: false, + } + } + + /// Decodes the buffer_in request, process the request delegating operations to an Struct implementing SftpServer, + /// serialises an answer in buffer_out and return the length usedd in buffer_out + pub async fn process( + &mut self, + buffer_in: &[u8], + buffer_out: &mut [u8], + ) -> WireResult { + if buffer_in.len() < 4 { + return Err(WireError::PacketWrong); + } + + let mut source = SftpSource { buffer: buffer_in, index: 0 }; + trace!("Source content: {:?}", source); + + let packet_length = u32::dec(&mut source)?; + trace!("Packet field lenght content: {}", packet_length); + + let mut sink = SftpSink::new(buffer_out); + + // TODO: Handle gracesfully unknow packets + let request = match SftpPacket::decode_request(&mut source) { + Ok(request) => { + info!("received request: {:?}", request); + request + } + Err(e) => { + warn!("Could not decode the request: {:?}", e); + return Err(e); + } + }; + + if !self.initialized && !matches!(request, SftpPacket::Init(_)) { + return Err(WireError::SSHProto); // TODO: Start using the SFTP Errors + } + + match request { + SftpPacket::Init(_) => { + // TODO: Do a real check, provide the lowest version or return an error if the client cannot handle the server SFTP_VERSION + let version = + SftpPacket::Version(InitVersionLowest { version: SFTP_VERSION }); + + info!("Sending '{:?}'", version); + + version.encode_response(ReqId(0), &mut sink)?; + + self.initialized = true; + } + SftpPacket::PathInfo(req_id, path_info) => { + let a_name = + T::realpath(path_info.path.as_str().expect( + "Could not deref and the errors are not harmonised", + )) + .await + .expect("Could not deref and the errors are not harmonised"); + + let response = SftpPacket::Name(req_id, a_name); + + response.encode_response(req_id, &mut sink)?; + } + _ => { + let response = SftpPacket::Status( + ReqId(0), + Status { + code: StatusCode::SSH_FX_OP_UNSUPPORTED, + message: "Not implemented".into(), + lang: "EN".into(), + }, + ); + response.encode_response(ReqId(0), &mut sink)?; + } + }; + + Ok(sink.finalise()) + } +} From b1995ce11044f02c4c9dd5d5e6b8e1201cd788a0 Mon Sep 17 00:00:00 2001 From: korbin Date: Tue, 26 Aug 2025 11:12:12 -0600 Subject: [PATCH 072/393] add method configuration servauth --- demo/common/src/server.rs | 5 ++-- src/conn.rs | 10 ++++++++ src/event.rs | 51 +++++++++++++++++++++++++++++++++++++++ src/runner.rs | 9 +++++++ src/servauth.rs | 7 +++++- 5 files changed, 79 insertions(+), 3 deletions(-) diff --git a/demo/common/src/server.rs b/demo/common/src/server.rs index c0c79040..f506be63 100644 --- a/demo/common/src/server.rs +++ b/demo/common/src/server.rs @@ -163,14 +163,15 @@ impl DemoCommon { a.reject() } - fn handle_firstauth(&self, a: ServFirstAuth) -> Result<()> { + fn handle_firstauth(&mut self, mut a: ServFirstAuth) -> Result<()> { let username = a.username()?; if !self.is_admin(username) && self.config.console_noauth { info!("Allowing auth for user {username}"); return a.allow(); }; - // a.pubkey().password() + // Explicitly enable password authentication + a.allow_password(true)?; Ok(()) } diff --git a/src/conn.rs b/src/conn.rs index 42d575b3..b6f03329 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -717,6 +717,16 @@ impl Conn { let p = self.packet(payload)?; self.server()?.auth.resume_pkok(p, s) } + + pub(crate) fn set_auth_methods( + &mut self, + password: bool, + pubkey: bool, + ) -> Result<()> { + let auth = &mut self.mut_server()?.auth; + auth.set_auth_methods(password, pubkey); + Ok(()) + } } #[cfg(test)] diff --git a/src/event.rs b/src/event.rs index b185e912..5105fc98 100644 --- a/src/event.rs +++ b/src/event.rs @@ -383,6 +383,23 @@ impl<'g, 'a> ServPasswordAuth<'g, 'a> { self.runner.resume_servauth(false) } + /// Enable or disable password authentication for subsequent attempts + pub fn allow_password(&mut self, enabled: bool) -> Result<()> { + let (_, pubkey) = self.runner.get_auth_methods()?; + self.runner.set_auth_methods(enabled, pubkey) + } + + /// Enable or disable public key authentication for subsequent attempts + pub fn allow_pubkey(&mut self, enabled: bool) -> Result<()> { + let (password, _) = self.runner.get_auth_methods()?; + self.runner.set_auth_methods(password, enabled) + } + + /// Configure which authentication methods are allowed for subsequent attempts + pub fn set_auth_methods(&mut self, password: bool, pubkey: bool) -> Result<()> { + self.runner.set_auth_methods(password, pubkey) + } + pub fn raw_username(&self) -> Result> { self.runner.fetch_servusername() } @@ -451,6 +468,23 @@ impl<'g, 'a> ServPubkeyAuth<'g, 'a> { self.runner.resume_servauth(false) } + /// Enable or disable password authentication for subsequent attempts + pub fn allow_password(&mut self, enabled: bool) -> Result<()> { + let (_, pubkey) = self.runner.get_auth_methods()?; + self.runner.set_auth_methods(enabled, pubkey) + } + + /// Enable or disable public key authentication for subsequent attempts + pub fn allow_pubkey(&mut self, enabled: bool) -> Result<()> { + let (password, _) = self.runner.get_auth_methods()?; + self.runner.set_auth_methods(password, enabled) + } + + /// Configure which authentication methods are allowed for subsequent attempts + pub fn set_auth_methods(&mut self, password: bool, pubkey: bool) -> Result<()> { + self.runner.set_auth_methods(password, pubkey) + } + pub fn raw_username(&self) -> Result> { self.runner.fetch_servusername() } @@ -500,6 +534,23 @@ impl<'g, 'a> ServFirstAuth<'g, 'a> { self.runner.resume_servauth(false) } + /// Enable or disable password authentication for this session + pub fn allow_password(&mut self, enabled: bool) -> Result<()> { + let (_, pubkey) = self.runner.get_auth_methods()?; + self.runner.set_auth_methods(enabled, pubkey) + } + + /// Enable or disable public key authentication for this session + pub fn allow_pubkey(&mut self, enabled: bool) -> Result<()> { + let (password, _) = self.runner.get_auth_methods()?; + self.runner.set_auth_methods(password, enabled) + } + + /// Configure which authentication methods are allowed + pub fn set_auth_methods(&mut self, password: bool, pubkey: bool) -> Result<()> { + self.runner.set_auth_methods(password, pubkey) + } + pub fn raw_username(&self) -> Result> { self.runner.fetch_servusername() } diff --git a/src/runner.rs b/src/runner.rs index 6cfcb9b1..917d8100 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -262,6 +262,15 @@ impl<'a> Runner<'a, server::Server> { self.traf_in.done_payload(); r } + + pub(crate) fn set_auth_methods(&mut self, password: bool, pubkey: bool) -> Result<()> { + self.conn.set_auth_methods(password, pubkey) + } + + pub(crate) fn get_auth_methods(&self) -> Result<(bool, bool)> { + let auth = &self.conn.server()?.auth; + Ok((auth.method_password, auth.method_pubkey)) + } } impl<'a, CS: CliServ> Runner<'a, CS> { diff --git a/src/servauth.rs b/src/servauth.rs index c415c2ac..749f026a 100644 --- a/src/servauth.rs +++ b/src/servauth.rs @@ -29,7 +29,6 @@ pub(crate) struct ServAuth { /// Username previously used, as an array of bytes pub username: Option>, - // TODO Add setters for methods in Runner/SSHServer creation. /// Whether to advertise password authentication and present it to the application /// /// Enabled by default @@ -53,6 +52,12 @@ impl Default for ServAuth { } impl ServAuth { + /// Configure which authentication methods are allowed + pub fn set_auth_methods(&mut self, password: bool, pubkey: bool) { + self.method_password = password; + self.method_pubkey = pubkey; + } + /// Returns an event for the app, or `DispatchEvent::None` if auth failure /// has been returned immediately. pub fn request( From a848fd6f0b8a7d2aadd2e0b1bf30aa3112a90a53 Mon Sep 17 00:00:00 2001 From: korbin Date: Thu, 28 Aug 2025 18:31:39 -0600 Subject: [PATCH 073/393] add servauth helpers --- src/event.rs | 25 +++++++++++++++++++++++++ src/packets.rs | 16 ++++++++++++++++ src/runner.rs | 6 +++++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/event.rs b/src/event.rs index 5105fc98..fdc5f08d 100644 --- a/src/event.rs +++ b/src/event.rs @@ -10,6 +10,7 @@ use self::{ use { crate::error::{Error, Result, TrapBug}, log::{debug, error, info, log, trace, warn}, + subtle::ConstantTimeEq, }; use core::fmt::Debug; @@ -362,6 +363,14 @@ impl<'g, 'a> ServPasswordAuth<'g, 'a> { self.raw_username()?.as_str() } + /// Perform a constant-time comparison of the user-presented username against a passed string. + pub fn matches_username( + &self, + username: impl core::convert::AsRef, + ) -> Result { + Ok(self.username()?.as_bytes().ct_eq(username.as_ref().as_bytes()).into()) + } + /// Retrieve the password presented by the user. /// /// When comparing with an expected password or hash, take @@ -371,6 +380,14 @@ impl<'g, 'a> ServPasswordAuth<'g, 'a> { self.raw_password()?.as_str() } + /// Perform a constant-time comparison of the user-presented password against a passed string. + pub fn matches_password( + &self, + password: impl core::convert::AsRef, + ) -> Result { + Ok(self.password()?.as_bytes().ct_eq(password.as_ref().as_bytes()).into()) + } + /// Accept the presented password. pub fn allow(mut self) -> Result<()> { self.done = true; @@ -516,6 +533,14 @@ impl<'g, 'a> ServFirstAuth<'g, 'a> { self.raw_username()?.as_str() } + /// Perform a constant-time comparison of the user-presented username against a passed string. + pub fn matches_username( + &self, + username: impl core::convert::AsRef, + ) -> Result { + Ok(self.username()?.as_bytes().ct_eq(username.as_ref().as_bytes()).into()) + } + /// Allow the user to log in. /// /// No further authentication challenges will be requested. diff --git a/src/packets.rs b/src/packets.rs index fa55c812..728e75b6 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -27,6 +27,7 @@ use sshnames::*; use sshwire::{BinString, Blob, TextString}; use sshwire::{SSHDecode, SSHEncode, SSHSink, SSHSource, WireError, WireResult}; use sshwire::{SSHDecodeEnum, SSHEncodeEnum}; +use subtle::ConstantTimeEq; #[cfg(feature = "rsa")] use rsa::traits::PublicKeyParts; @@ -361,6 +362,21 @@ impl PubKey<'_> { }; Ok(m) } + + #[cfg(feature = "openssh-key")] + pub fn fingerprint( + &self, + hash_alg: ssh_key::HashAlg, + ) -> Result { + let ssh_key: ssh_key::PublicKey = self.try_into()?; + + Ok(ssh_key.fingerprint(hash_alg)) + } + + #[cfg(feature = "openssh-key")] + pub fn matches_fingerprint(&self, fp: &ssh_key::Fingerprint) -> Result { + Ok(self.fingerprint(fp.algorithm())?.as_bytes().ct_eq(fp.as_bytes()).into()) + } } // ssh_key::PublicKey is used for known_hosts comparisons diff --git a/src/runner.rs b/src/runner.rs index 917d8100..939ee460 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -263,7 +263,11 @@ impl<'a> Runner<'a, server::Server> { r } - pub(crate) fn set_auth_methods(&mut self, password: bool, pubkey: bool) -> Result<()> { + pub(crate) fn set_auth_methods( + &mut self, + password: bool, + pubkey: bool, + ) -> Result<()> { self.conn.set_auth_methods(password, pubkey) } From 1954507ba13fe8afa509884e280f72a0afdec037 Mon Sep 17 00:00:00 2001 From: korbin Date: Fri, 5 Sep 2025 12:41:28 -0600 Subject: [PATCH 074/393] rename allow_password/allow_pubkey, add caution docs re: user enumeration attacks --- demo/common/src/server.rs | 2 +- src/event.rs | 66 ++++++++++++++++++++++++++++++--------- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/demo/common/src/server.rs b/demo/common/src/server.rs index f506be63..aec02d66 100644 --- a/demo/common/src/server.rs +++ b/demo/common/src/server.rs @@ -171,7 +171,7 @@ impl DemoCommon { }; // Explicitly enable password authentication - a.allow_password(true)?; + a.enable_password_auth(true)?; Ok(()) } diff --git a/src/event.rs b/src/event.rs index fdc5f08d..4e79951c 100644 --- a/src/event.rs +++ b/src/event.rs @@ -400,19 +400,31 @@ impl<'g, 'a> ServPasswordAuth<'g, 'a> { self.runner.resume_servauth(false) } - /// Enable or disable password authentication for subsequent attempts - pub fn allow_password(&mut self, enabled: bool) -> Result<()> { + /// Enable or disable password authentication for subsequent attempts. + /// + /// # Caution + /// Enabling or disabling authentication methods based on username can + /// unintentionally enable user enumeration attacks. + pub fn enable_password_auth(&mut self, enabled: bool) -> Result<()> { let (_, pubkey) = self.runner.get_auth_methods()?; self.runner.set_auth_methods(enabled, pubkey) } - /// Enable or disable public key authentication for subsequent attempts - pub fn allow_pubkey(&mut self, enabled: bool) -> Result<()> { + /// Enable or disable public key authentication for subsequent attempts. + /// + /// # Caution + /// Enabling or disabling authentication methods based on username can + /// unintentionally enable user enumeration attacks. + pub fn enable_pubkey_auth(&mut self, enabled: bool) -> Result<()> { let (password, _) = self.runner.get_auth_methods()?; self.runner.set_auth_methods(password, enabled) } - /// Configure which authentication methods are allowed for subsequent attempts + /// Configure which authentication methods are allowed for subsequent attempts. + /// + /// # Caution + /// Enabling or disabling authentication methods based on username can + /// unintentionally enable user enumeration attacks. pub fn set_auth_methods(&mut self, password: bool, pubkey: bool) -> Result<()> { self.runner.set_auth_methods(password, pubkey) } @@ -485,19 +497,31 @@ impl<'g, 'a> ServPubkeyAuth<'g, 'a> { self.runner.resume_servauth(false) } - /// Enable or disable password authentication for subsequent attempts - pub fn allow_password(&mut self, enabled: bool) -> Result<()> { + /// Enable or disable password authentication for subsequent attempts. + /// + /// # Caution + /// Enabling or disabling authentication methods based on username can + /// unintentionally enable user enumeration attacks. + pub fn enable_password_auth(&mut self, enabled: bool) -> Result<()> { let (_, pubkey) = self.runner.get_auth_methods()?; self.runner.set_auth_methods(enabled, pubkey) } - /// Enable or disable public key authentication for subsequent attempts - pub fn allow_pubkey(&mut self, enabled: bool) -> Result<()> { + /// Enable or disable public key authentication for subsequent attempts. + /// + /// # Caution + /// Enabling or disabling authentication methods based on username can + /// unintentionally enable user enumeration attacks. + pub fn enable_pubkey_auth(&mut self, enabled: bool) -> Result<()> { let (password, _) = self.runner.get_auth_methods()?; self.runner.set_auth_methods(password, enabled) } - /// Configure which authentication methods are allowed for subsequent attempts + /// Configure which authentication methods are allowed for subsequent attempts. + /// + /// # Caution + /// Enabling or disabling authentication methods based on username can + /// unintentionally enable user enumeration attacks. pub fn set_auth_methods(&mut self, password: bool, pubkey: bool) -> Result<()> { self.runner.set_auth_methods(password, pubkey) } @@ -559,19 +583,31 @@ impl<'g, 'a> ServFirstAuth<'g, 'a> { self.runner.resume_servauth(false) } - /// Enable or disable password authentication for this session - pub fn allow_password(&mut self, enabled: bool) -> Result<()> { + /// Enable or disable password authentication for this session. + /// + /// # Caution + /// Enabling or disabling authentication methods based on username can + /// unintentionally enable user enumeration attacks. + pub fn enable_password_auth(&mut self, enabled: bool) -> Result<()> { let (_, pubkey) = self.runner.get_auth_methods()?; self.runner.set_auth_methods(enabled, pubkey) } - /// Enable or disable public key authentication for this session - pub fn allow_pubkey(&mut self, enabled: bool) -> Result<()> { + /// Enable or disable public key authentication for this session. + /// + /// # Caution + /// Enabling or disabling authentication methods based on username can + /// unintentionally enable user enumeration attacks. + pub fn enable_pubkey_auth(&mut self, enabled: bool) -> Result<()> { let (password, _) = self.runner.get_auth_methods()?; self.runner.set_auth_methods(password, enabled) } - /// Configure which authentication methods are allowed + /// Configure which authentication methods are allowed. + /// + /// # Caution + /// Enabling or disabling authentication methods based on username can + /// unintentionally enable user enumeration attacks. pub fn set_auth_methods(&mut self, password: bool, pubkey: bool) -> Result<()> { self.runner.set_auth_methods(password, pubkey) } From 8ca6cfb41b61f58a1cc12f5d390dd5d3c954d5ed Mon Sep 17 00:00:00 2001 From: korbin Date: Fri, 5 Sep 2025 12:42:11 -0600 Subject: [PATCH 075/393] remove matches_fingerprint --- src/packets.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index 728e75b6..2431778f 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -372,11 +372,6 @@ impl PubKey<'_> { Ok(ssh_key.fingerprint(hash_alg)) } - - #[cfg(feature = "openssh-key")] - pub fn matches_fingerprint(&self, fp: &ssh_key::Fingerprint) -> Result { - Ok(self.fingerprint(fp.algorithm())?.as_bytes().ct_eq(fp.as_bytes()).into()) - } } // ssh_key::PublicKey is used for known_hosts comparisons From 056503e4d0bb07331f113a84a569fff9131055fb Mon Sep 17 00:00:00 2001 From: korbin Date: Fri, 5 Sep 2025 12:45:22 -0600 Subject: [PATCH 076/393] add usage docs for matches_password --- src/event.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/event.rs b/src/event.rs index 4e79951c..227453a3 100644 --- a/src/event.rs +++ b/src/event.rs @@ -381,6 +381,9 @@ impl<'g, 'a> ServPasswordAuth<'g, 'a> { } /// Perform a constant-time comparison of the user-presented password against a passed string. + /// # Caution + /// This is better than a naive comparison, but passwords should be hashed and stored using a + /// platform-appropriate password hashing function. Consider bcrypt, argon2, or pbkdf2. pub fn matches_password( &self, password: impl core::convert::AsRef, From b3f74c146e1b1cc412d7524056f0d6441ebc3404 Mon Sep 17 00:00:00 2001 From: korbin Date: Fri, 5 Sep 2025 12:48:36 -0600 Subject: [PATCH 077/393] make matches_password return a bool --- src/event.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/event.rs b/src/event.rs index 227453a3..2d66bfea 100644 --- a/src/event.rs +++ b/src/event.rs @@ -387,8 +387,11 @@ impl<'g, 'a> ServPasswordAuth<'g, 'a> { pub fn matches_password( &self, password: impl core::convert::AsRef, - ) -> Result { - Ok(self.password()?.as_bytes().ct_eq(password.as_ref().as_bytes()).into()) + ) -> bool { + match self.password() { + Ok(p) => p.as_bytes().ct_eq(password.as_ref().as_bytes()).into(), + _ => false, + } } /// Accept the presented password. From 351e2ef182d1d11ca34bdc1538972246bbae93d7 Mon Sep 17 00:00:00 2001 From: korbin Date: Fri, 5 Sep 2025 12:50:23 -0600 Subject: [PATCH 078/393] make matches_username return a bool --- src/event.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/event.rs b/src/event.rs index 2d66bfea..c1d74b7c 100644 --- a/src/event.rs +++ b/src/event.rs @@ -367,8 +367,11 @@ impl<'g, 'a> ServPasswordAuth<'g, 'a> { pub fn matches_username( &self, username: impl core::convert::AsRef, - ) -> Result { - Ok(self.username()?.as_bytes().ct_eq(username.as_ref().as_bytes()).into()) + ) -> bool { + match self.username() { + Ok(u) => u.as_bytes().ct_eq(username.as_ref().as_bytes()).into(), + _ => false, + } } /// Retrieve the password presented by the user. @@ -567,8 +570,11 @@ impl<'g, 'a> ServFirstAuth<'g, 'a> { pub fn matches_username( &self, username: impl core::convert::AsRef, - ) -> Result { - Ok(self.username()?.as_bytes().ct_eq(username.as_ref().as_bytes()).into()) + ) -> bool { + match self.username() { + Ok(u) => u.as_bytes().ct_eq(username.as_ref().as_bytes()).into(), + _ => false, + } } /// Allow the user to log in. From e2d5a97be687a22a7ddf55ce8fd9aa8b8d9dcc22 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Sat, 6 Sep 2025 16:41:00 +1000 Subject: [PATCH 079/393] Adding as_str for Filename Just gets the inner BinString as_str --- sftp/src/proto.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 6fff1bda..a415b0a8 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -18,6 +18,12 @@ impl<'a> From<&'a str> for Filename<'a> { } } +impl<'a> Filename<'a> { + pub fn as_str(&self) -> Result<&'a str, WireError> { + core::str::from_utf8(self.0.0).map_err(|_| WireError::BadString) + } +} + #[derive(Debug, SSHEncode, SSHDecode)] pub struct FileHandle<'a>(pub BinString<'a>); From e48f0e8085ac47d02570ae1fcabd3dbba6a2b8ae Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Sat, 6 Sep 2025 16:44:46 +1000 Subject: [PATCH 080/393] Handling the conditions where the SFTP protocol fails, setting things up so it can be handle and will wait for a new channel on the pipe from the program loop --- demo/sftp/std/src/main.rs | 53 +++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 85b7021d..e59e8c92 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -139,25 +139,22 @@ impl DemoServer for StdDemo { }; let sftp_loop = async { - let ch = chan_pipe.receive().await; - info!("SFTP loop has received a channel handle"); - - let mut stdio = serv.stdio(ch).await?; - let mut buffer_in = [0u8; 1000]; - let mut buffer_out = [0u8; 1000]; - - let mut sftp_handler = - SftpHandler::::new(&buffer_in, &mut buffer_out); - loop { - let lr = stdio.read(&mut buffer_in).await?; - debug!("SFTP <---- received: {:?}", &buffer_in[0..lr]); + let ch = chan_pipe.receive().await; - let lw = - sftp_handler.process(&buffer_in[0..lr], &mut buffer_out).await?; + info!("SFTP loop has received a channel handle {:?}", ch.num()); - stdio.write(&mut buffer_out[0..lw]).await?; - debug!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); + let buffer_in = [0u8; 1000]; + let buffer_out = [0u8; 1000]; + + match sftp_server_loop(serv, buffer_in, buffer_out, ch).await { + Ok(_) => { + warn!("sftp server loop finished gracefully"); + } + Err(e) => { + warn!("sftp server loop finished with an error: {}", e) + } + }; } Ok::<_, Error>(()) }; @@ -168,6 +165,30 @@ impl DemoServer for StdDemo { } } +async fn sftp_server_loop( + serv: &SSHServer<'_>, + mut buffer_in: [u8; 1000], + mut buffer_out: [u8; 1000], + ch: ChanHandle, +) -> Result<(), Error> { + let mut stdio = serv.stdio(ch).await?; + let mut sftp_handler = + SftpHandler::::new(&buffer_in, &mut buffer_out); + Ok(loop { + let lr = stdio.read(&mut buffer_in).await?; + debug!("SFTP <---- received: {:?}", &buffer_in[0..lr]); + if lr == 0 { + debug!("client disconnected"); + return Ok(()); + } + + let lw = sftp_handler.process(&buffer_in[0..lr], &mut buffer_out).await?; + + stdio.write(&mut buffer_out[0..lw]).await?; + debug!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); + }) +} + // TODO: pool_size should be NUM_LISTENERS but needs a literal #[embassy_executor::task(pool_size = 4)] async fn listen( From f3e6b213930c14aead1634d74d5a7e737a50a375 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Sat, 6 Sep 2025 16:48:50 +1000 Subject: [PATCH 081/393] removing std uses --- sftp/src/sftphandle.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index a83086bd..11358a02 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -5,9 +5,10 @@ use crate::sftpserver::{ItemHandle, SftpServer}; use sunset::sshwire::{SSHDecode, SSHSink, SSHSource, WireError, WireResult}; +use core::marker::PhantomData; +use core::u32; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -use std::marker::PhantomData; #[derive(Default, Debug)] pub struct SftpSource<'de> { From ff41e437d14d436a66ac30adb77bffe1ac676212 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 05:45:13 +1000 Subject: [PATCH 082/393] Reformed sftpserver - flags are no longer required - removed ItemHandle - Added Into and TryFrom File Handle for SftpServer trait - Added lifetime 'a to Handle and the trait associated function taking or returning the type minor changes in proto: added clone, copy and compare implements to FileHandle and TextString --- sftp/src/proto.rs | 4 ++-- sftp/src/sftpserver.rs | 42 ++++++++++++++++++++++-------------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index a415b0a8..c9ea2192 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -24,7 +24,7 @@ impl<'a> Filename<'a> { } } -#[derive(Debug, SSHEncode, SSHDecode)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, SSHEncode, SSHDecode)] pub struct FileHandle<'a>(pub BinString<'a>); /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 @@ -86,7 +86,7 @@ pub struct Status<'a> { pub lang: TextString<'a>, } -#[derive(Debug, SSHEncode, SSHDecode)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, SSHEncode, SSHDecode)] pub struct Handle<'a> { pub handle: FileHandle<'a>, } diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index f69342c3..72b1593c 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,41 +1,48 @@ -use crate::proto::{Attrs, Name, StatusCode}; +use crate::proto::{Attrs, FileHandle, Name, StatusCode}; +use core::fmt::Debug; use core::marker::PhantomData; -pub type SftpResult = core::result::Result; +pub type SftpOpResult = core::result::Result; /// All trait functions are optional in the SFTP protocol. /// Some less core operations have a Provided implementation returning /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. pub trait SftpServer { - type Handle; + // type Handle: Into + TryFrom + Debug; + type Handle<'a>: Into> + TryFrom> + Debug + Copy; // TODO flags struct - async fn open( + async fn open<'a>( filename: &str, - flags: u32, attrs: &Attrs, - ) -> SftpResult; + ) -> SftpOpResult>; /// Close either a file or directory handle - async fn close(handle: &Self::Handle) -> SftpResult<()>; + async fn close<'a>(handle: &Self::Handle<'a>) -> SftpOpResult<()>; - async fn read( - handle: &Self::Handle, + async fn read<'a>( + handle: &Self::Handle<'a>, offset: u64, reply: &mut ReadReply, - ) -> SftpResult<()>; + ) -> SftpOpResult<()>; - async fn write(handle: &Self::Handle, offset: u64, buf: &[u8]) - -> SftpResult<()>; + async fn write<'a>( + handle: &Self::Handle<'a>, + offset: u64, + buf: &[u8], + ) -> SftpOpResult<()>; - async fn opendir(dir: &str) -> SftpResult; + async fn opendir<'a>(dir: &str) -> SftpOpResult>; - async fn readdir(handle: &Self::Handle, reply: &mut DirReply) -> SftpResult<()>; + async fn readdir<'a>( + handle: &Self::Handle<'a>, + reply: &mut DirReply, + ) -> SftpOpResult<()>; /// Provides the real path of the directory specified - async fn realpath(dir: &str) -> SftpResult>; + async fn realpath(dir: &str) -> SftpOpResult>; } pub struct ReadReply<'g, 'a> { @@ -59,8 +66,3 @@ pub struct ChanOut<'g, 'a> { _phantom_g: PhantomData<&'g ()>, _phantom_a: PhantomData<&'a ()>, } - -#[derive(Debug)] -pub struct ItemHandle { - client_opaque_handle: String, -} From 5645b9b25d3705f5e2204657eca1b59bc134ca4b Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 05:49:07 +1000 Subject: [PATCH 083/393] Bug fix in SftpPacket.encode_request enconde+request was encoding sftp number and the request id, which is incoded in self.enc(c) call. Fixed and removed req_id from parameter list --- sftp/src/proto.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index c9ea2192..b098fb86 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -606,7 +606,7 @@ macro_rules! sftpmessages { /// Used by a SFTP server. Does not include the length field. /// /// Fails if the encoded SFTP Packet is not a response - pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { + pub fn encode_response(&self, s: &mut dyn SSHSink) -> WireResult<()> { if !self.sftp_num().is_response() { return Err(WireError::PacketWrong) @@ -615,11 +615,6 @@ macro_rules! sftpmessages { // therefore a bug, bug Error::bug() is not compatible with WireResult } - // packet type - self.sftp_num().enc(s)?; - // request ID - id.0.enc(s)?; - // contents self.enc(s) } From 4d700bd06ed80d5b44a6fd2c916d815c7b1219d8 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 05:56:32 +1000 Subject: [PATCH 084/393] Working on sftphandle - handling unsupported packet - fixing calls to SftpPacket.encode_request - adding 'a lifetime to SftpHandler to handle a Vec of FileHandle<'a> --- sftp/src/sftphandle.rs | 105 +++++++++++++++++++++++++++++------------ 1 file changed, 76 insertions(+), 29 deletions(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 11358a02..4aea15ae 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,7 +1,8 @@ use crate::proto::{ - InitVersionLowest, ReqId, SFTP_VERSION, SftpPacket, Status, StatusCode, + self, FileHandle, InitVersionLowest, ReqId, SFTP_VERSION, SftpPacket, Status, + StatusCode, }; -use crate::sftpserver::{ItemHandle, SftpServer}; +use crate::sftpserver::SftpServer; use sunset::sshwire::{SSHDecode, SSHSink, SSHSource, WireError, WireResult}; @@ -62,8 +63,7 @@ impl<'g> SftpSink<'g> { const LENG_FIELD_LEN: usize = 4; // TODO: Move it to a better location pub fn new(s: &'g mut [u8]) -> Self { - SftpSink { buffer: s, index: 0 } - // SftpSink { buffer: s, index: SftpSink::LENG_FIELD_LEN } + SftpSink { buffer: s, index: Self::LENG_FIELD_LEN } } /// Finalise the buffer by prepending the payload size and returning @@ -91,26 +91,27 @@ impl<'g> SSHSink for SftpSink<'g> { if v.len() + self.index > self.buffer.len() { return Err(WireError::NoRoom); } + trace!("Sink index: {:}", self.index); v.iter().for_each(|val| { self.buffer[self.index] = *val; self.index += 1; }); - {} + trace!("Sink new index: {:}", self.index); Ok(()) } } -#[derive(Debug)] -pub struct SftpHandler +#[derive(Debug, Clone)] +pub struct SftpHandler<'a, T> where T: SftpServer, { server_type: PhantomData, - handle_list: Vec, + handle_list: Vec>, initialized: bool, } -impl SftpHandler +impl<'a, T> SftpHandler<'a, T> where T: SftpServer, { @@ -142,21 +143,37 @@ where let mut sink = SftpSink::new(buffer_out); // TODO: Handle gracesfully unknow packets - let request = match SftpPacket::decode_request(&mut source) { + match SftpPacket::decode_request(&mut source) { Ok(request) => { info!("received request: {:?}", request); - request - } - Err(e) => { - warn!("Could not decode the request: {:?}", e); - return Err(e); + self.process_known_request(&mut sink, request).await?; } + Err(e) => match e { + WireError::UnknownPacket { number } => { + warn!("Unsuported Packet Number {:?} {:?}", number, e); + push_unsuported(ReqId(u32::MAX), &mut sink)?; + } + _ => { + error!("Could not decode the request: {:?}", e); + return Err(e); + } + }, }; + Ok(sink.finalise()) + } + + async fn process_known_request( + &mut self, + sink: &mut SftpSink<'_>, + request: SftpPacket<'_>, + ) -> Result<(), WireError> + where + T: SftpServer, + { if !self.initialized && !matches!(request, SftpPacket::Init(_)) { return Err(WireError::SSHProto); // TODO: Start using the SFTP Errors } - match request { SftpPacket::Init(_) => { // TODO: Do a real check, provide the lowest version or return an error if the client cannot handle the server SFTP_VERSION @@ -165,7 +182,7 @@ where info!("Sending '{:?}'", version); - version.encode_response(ReqId(0), &mut sink)?; + version.encode_response(sink)?; self.initialized = true; } @@ -179,21 +196,51 @@ where let response = SftpPacket::Name(req_id, a_name); - response.encode_response(req_id, &mut sink)?; + response.encode_response(sink)?; + } + SftpPacket::Open(req_id, open) => { + match T::open(open.filename.as_str()?, &open.attrs).await { + Ok(handle) => { + self.handle_list.push(handle.into()); + let response = SftpPacket::Handle( + req_id, + proto::Handle { handle: handle.into() }, + ); + response.encode_response(sink)?; + info!("Sending '{:?}'", response); + } + Err(status_code) => { + let response = SftpPacket::Status( + req_id, + Status { + code: status_code, + message: "".into(), + lang: "EN".into(), + }, + ); + response.encode_request(req_id, sink)?; + info!("Sending '{:?}'", response); + } + }; + // push_unsuported(req_id, sink)?; } _ => { - let response = SftpPacket::Status( - ReqId(0), - Status { - code: StatusCode::SSH_FX_OP_UNSUPPORTED, - message: "Not implemented".into(), - lang: "EN".into(), - }, - ); - response.encode_response(ReqId(0), &mut sink)?; + push_unsuported(ReqId(0), sink)?; } }; - - Ok(sink.finalise()) + Ok(()) } } + +fn push_unsuported(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireError> { + let response = SftpPacket::Status( + req_id, + Status { + code: StatusCode::SSH_FX_OP_UNSUPPORTED, + message: "Not implemented".into(), + lang: "EN".into(), + }, + ); + response.encode_response(sink)?; + Ok(()) +} From 49a8295864a82f277be6703aa6762dc2a60384cf Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 05:56:41 +1000 Subject: [PATCH 085/393] sftp.rs - removed ItemHandle; + Adding StatusCode + FileHandle --- sftp/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 625fe3df..1501b72d 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,17 +1,19 @@ mod proto; +mod sftperror; mod sftphandle; mod sftpserver; pub use sftpserver::DirReply; -pub use sftpserver::ItemHandle; pub use sftpserver::ReadReply; -pub use sftpserver::SftpResult; +pub use sftpserver::SftpOpResult; pub use sftpserver::SftpServer; pub use sftphandle::SftpHandler; pub use proto::Attrs; +pub use proto::FileHandle; pub use proto::Filename; pub use proto::Name; pub use proto::NameEntry; pub use proto::PathInfo; +pub use proto::StatusCode; From ecad80764c5278497d3c8556dd6a986958201bf8 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 05:57:10 +1000 Subject: [PATCH 086/393] minor fix in decode_request return value --- sftp/src/proto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index b098fb86..65125ca6 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -583,7 +583,7 @@ macro_rules! sftpmessages { /// Used by a SFTP server. Does not include the length field. /// /// It will fail if the received packet is a response - pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(Self)> + pub fn decode_request<'de, S>(s: &mut S) -> WireResult where S: SSHSource<'de>, 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes From 55ff40a50bd8dd0c05bc47bfef24f4179e0b88b0 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 06:07:59 +1000 Subject: [PATCH 087/393] changes in demosftpserver very unstable at this point --- demo/sftp/std/src/demosftpserver.rs | 42 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 44318953..19fd22dd 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -1,9 +1,6 @@ -use std::str::FromStr; - -use sunset::TextString; use sunset_sftp::{ - Attrs, DirReply, Filename, ItemHandle, Name, NameEntry, ReadReply, SftpResult, - SftpServer, + Attrs, DirReply, FileHandle, Filename, Name, NameEntry, ReadReply, SftpOpResult, + SftpServer, StatusCode, }; #[allow(unused_imports)] @@ -12,48 +9,49 @@ use log::{debug, error, info, log, trace, warn}; pub struct DemoSftpServer {} impl SftpServer for DemoSftpServer { - type Handle = ItemHandle; + type Handle<'a> = FileHandle<'a>; - async fn open( + async fn open<'a>( filename: &str, - flags: u32, + // flags: u32, attrs: &Attrs, - ) -> SftpResult { - todo!() + ) -> SftpOpResult> { + warn!("Wont allow open!"); + Err(StatusCode::SSH_FX_PERMISSION_DENIED) } - async fn close(handle: &Self::Handle) -> SftpResult<()> { + async fn close<'a>(handle: &Self::Handle<'a>) -> SftpOpResult<()> { todo!() } - async fn read( - handle: &Self::Handle, + async fn read<'a>( + handle: &Self::Handle<'a>, offset: u64, reply: &mut ReadReply<'_, '_>, - ) -> SftpResult<()> { + ) -> SftpOpResult<()> { todo!() } - async fn write( - handle: &Self::Handle, + async fn write<'a>( + handle: &Self::Handle<'a>, offset: u64, buf: &[u8], - ) -> SftpResult<()> { + ) -> SftpOpResult<()> { todo!() } - async fn opendir(dir: &str) -> SftpResult { + async fn opendir<'a>(dir: &str) -> SftpOpResult> { todo!() } - async fn readdir( - handle: &Self::Handle, + async fn readdir<'a>( + handle: &Self::Handle<'a>, reply: &mut DirReply<'_, '_>, - ) -> SftpResult<()> { + ) -> SftpOpResult<()> { todo!() } - async fn realpath(dir: &str) -> SftpResult> { + async fn realpath(dir: &str) -> SftpOpResult> { debug!("finding path for: {:?}", dir); Ok(Name(vec![NameEntry { filename: Filename::from("/root/just/kidding"), From e2fde1b9a6a3fbad50558277417abd01773b048d Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 06:21:31 +1000 Subject: [PATCH 088/393] handling and logging unsupported or uninitialised messages --- sftp/src/sftphandle.rs | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 4aea15ae..b6e698e9 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -142,7 +142,6 @@ where let mut sink = SftpSink::new(buffer_out); - // TODO: Handle gracesfully unknow packets match SftpPacket::decode_request(&mut source) { Ok(request) => { info!("received request: {:?}", request); @@ -150,12 +149,12 @@ where } Err(e) => match e { WireError::UnknownPacket { number } => { - warn!("Unsuported Packet Number {:?} {:?}", number, e); + warn!("Error decoding SFTP Packet:{:?}", e); push_unsuported(ReqId(u32::MAX), &mut sink)?; } _ => { - error!("Could not decode the request: {:?}", e); - return Err(e); + error!("Error decoding SFTP Packet: {:?}", e); + push_unsuported(ReqId(u32::MAX), &mut sink)?; } }, }; @@ -172,7 +171,9 @@ where T: SftpServer, { if !self.initialized && !matches!(request, SftpPacket::Init(_)) { - return Err(WireError::SSHProto); // TODO: Start using the SFTP Errors + push_general_failure(ReqId(u32::MAX), "Not Initialized", sink)?; + error!("Request sent before init: {:?}", request); + return Ok(()); } match request { SftpPacket::Init(_) => { @@ -241,6 +242,25 @@ fn push_unsuported(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireErr lang: "EN".into(), }, ); + debug!("Pushing a unsupported status message: {:?}", response); + response.encode_response(sink)?; + Ok(()) +} + +fn push_general_failure( + req_id: ReqId, + msg: &'static str, + sink: &mut SftpSink<'_>, +) -> Result<(), WireError> { + let response = SftpPacket::Status( + req_id, + Status { + code: StatusCode::SSH_FX_FAILURE, + message: msg.into(), + lang: "EN".into(), + }, + ); + debug!("Pushing a general failure status message: {:?}", response); response.encode_response(sink)?; Ok(()) } From 1da7cabf2e073a54df730747ff11d22447bb336b Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 07:35:45 +1000 Subject: [PATCH 089/393] fix bug encoding_request->encoding_response --- sftp/src/sftphandle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index b6e698e9..22c0896e 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -219,7 +219,7 @@ where lang: "EN".into(), }, ); - response.encode_request(req_id, sink)?; + response.encode_response(sink)?; info!("Sending '{:?}'", response); } }; From 440e401ab2faf7f79e131160595e7be38c7ee658 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 07:37:35 +1000 Subject: [PATCH 090/393] Adding trait implementation - requires instance: WIP - catches default behaviour: Unsupported --- sftp/src/sftpserver.rs | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 72b1593c..c669dcdd 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -10,39 +10,63 @@ pub type SftpOpResult = core::result::Result; /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. pub trait SftpServer { - // type Handle: Into + TryFrom + Debug; type Handle<'a>: Into> + TryFrom> + Debug + Copy; - // TODO flags struct + /// Opens a file or directory for reading/writing async fn open<'a>( + &mut self, filename: &str, attrs: &Attrs, - ) -> SftpOpResult>; + ) -> SftpOpResult> { + log::error!("SftpServer Open operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } /// Close either a file or directory handle - async fn close<'a>(handle: &Self::Handle<'a>) -> SftpOpResult<()>; + async fn close<'a>(&mut self, handle: &Self::Handle<'a>) -> SftpOpResult<()> { + log::error!("SftpServer Close operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } async fn read<'a>( + &mut self, handle: &Self::Handle<'a>, offset: u64, reply: &mut ReadReply, - ) -> SftpOpResult<()>; + ) -> SftpOpResult<()> { + log::error!("SftpServer Read operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } async fn write<'a>( + &mut self, handle: &Self::Handle<'a>, offset: u64, buf: &[u8], - ) -> SftpOpResult<()>; + ) -> SftpOpResult<()> { + log::error!("SftpServer Write operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } - async fn opendir<'a>(dir: &str) -> SftpOpResult>; + async fn opendir<'a>(&mut self, dir: &str) -> SftpOpResult> { + log::error!("SftpServer OpenDir operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } async fn readdir<'a>( + &mut self, handle: &Self::Handle<'a>, reply: &mut DirReply, - ) -> SftpOpResult<()>; + ) -> SftpOpResult<()> { + log::error!("SftpServer ReadDir operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } /// Provides the real path of the directory specified - async fn realpath(dir: &str) -> SftpOpResult>; + async fn realpath(&mut self, dir: &str) -> SftpOpResult> { + log::error!("SftpServer RealPath operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } } pub struct ReadReply<'g, 'a> { From 4511bd2f661485a3615b4eac9e0a341ec905d99c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 15:00:47 +1000 Subject: [PATCH 091/393] Simplifying SftpServer trait async and the Handle are out for now I would like to keep those but since I want a structured of SftpServer to keep it's own internal state, I could not use async or a Handle with a trait definition. It is unfortunately unstable. Still a WIP but now a client can PUT a small file and its content will be displayed in debug. For now the buffers overflow on long SSH_FXP_WRITE packets and fail after trying to decode in the next loop iteration more of the same packet content. The previous sftpserver trait has been stored in asyncsftpserver.rs --- demo/sftp/std/src/demosftpserver.rs | 140 ++++++++++++++++++++-------- demo/sftp/std/src/main.rs | 5 +- sftp/src/asyncsftpserver.rs | 87 +++++++++++++++++ sftp/src/lib.rs | 1 - sftp/src/sftphandle.rs | 110 +++++++++++++--------- sftp/src/sftpserver.rs | 68 ++++++++------ 6 files changed, 297 insertions(+), 114 deletions(-) create mode 100644 sftp/src/asyncsftpserver.rs diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 19fd22dd..bb846ab9 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -1,3 +1,4 @@ +use sunset::sshwire::BinString; use sunset_sftp::{ Attrs, DirReply, FileHandle, Filename, Name, NameEntry, ReadReply, SftpOpResult, SftpServer, StatusCode, @@ -6,65 +7,124 @@ use sunset_sftp::{ #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -pub struct DemoSftpServer {} - -impl SftpServer for DemoSftpServer { - type Handle<'a> = FileHandle<'a>; +pub struct DemoSftpServer { + valid_handlers: Vec, + user_path: String, +} - async fn open<'a>( +impl DemoSftpServer { + pub fn new(user: String) -> Self { + DemoSftpServer { + valid_handlers: vec![], + user_path: format!("/{}/", user.clone()), + } + } +} +impl SftpServer<'_> for DemoSftpServer { + // Mocking an Open operation. Will not check for permissions + fn open( + &mut self, filename: &str, - // flags: u32, - attrs: &Attrs, - ) -> SftpOpResult> { - warn!("Wont allow open!"); - Err(StatusCode::SSH_FX_PERMISSION_DENIED) + _attrs: &Attrs, + ) -> SftpOpResult> { + if self.valid_handlers.contains(&filename.to_string()) { + warn!("File {:?} already open, won't allow it", filename); + return Err(StatusCode::SSH_FX_PERMISSION_DENIED); + } + + self.valid_handlers.push(filename.to_string()); + + let fh = FileHandle(BinString( + self.valid_handlers.last().expect("just pushed an element").as_bytes(), + )); + Ok(fh) + } + + fn realpath(&mut self, dir: &str) -> SftpOpResult> { + debug!("finding path for: {:?}", dir); + Ok(Name(vec![NameEntry { + filename: Filename::from(self.user_path.as_str()), + _longname: Filename::from(""), + attrs: Attrs { + size: None, + uid: None, + gid: None, + permissions: None, + atime: None, + mtime: None, + ext_count: None, + }, + }])) } - async fn close<'a>(handle: &Self::Handle<'a>) -> SftpOpResult<()> { - todo!() + fn close(&mut self, handle: &FileHandle) -> SftpOpResult<()> { + let initial_count = self.valid_handlers.len(); + if initial_count == 0 { + log::error!( + "SftpServer Close operation with no handles stored: handle = {:?}", + handle + ); + return Err(StatusCode::SSH_FX_FAILURE); + } + + let filename = + String::from_utf8(handle.0.as_ref().to_vec()).unwrap_or("".into()); + + if !self.valid_handlers.contains(&filename) { + log::error!( + "SftpServer Close operation could not match an stored handler: handle = {:?}", + handle + ); + return Err(StatusCode::SSH_FX_FAILURE); + } + self.valid_handlers.retain(|handler| handler.ne(&filename)); + log::debug!("SftpServer Close operation on {:?} was successful", filename); + Ok(()) } - async fn read<'a>( - handle: &Self::Handle<'a>, + fn read( + &mut self, + handle: &FileHandle, offset: u64, reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { - todo!() + log::error!( + "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", + handle, + offset + ); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - async fn write<'a>( - handle: &Self::Handle<'a>, + fn write( + &mut self, + handle: &FileHandle, offset: u64, buf: &[u8], ) -> SftpOpResult<()> { - todo!() + log::debug!( + "SftpServer Write operation: handle = {:?}, offset = {:?}, buf = {:?}", + handle, + offset, + String::from_utf8(buf.to_vec()) + ); + Ok(()) } - async fn opendir<'a>(dir: &str) -> SftpOpResult> { - todo!() + fn opendir(&mut self, dir: &str) -> SftpOpResult> { + log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - async fn readdir<'a>( - handle: &Self::Handle<'a>, + fn readdir( + &mut self, + handle: &FileHandle, reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { - todo!() - } - - async fn realpath(dir: &str) -> SftpOpResult> { - debug!("finding path for: {:?}", dir); - Ok(Name(vec![NameEntry { - filename: Filename::from("/root/just/kidding"), - _longname: Filename::from(""), - attrs: Attrs { - size: None, - uid: None, - gid: None, - permissions: None, - atime: None, - mtime: None, - ext_count: None, - }, - }])) + log::error!( + "SftpServer ReadDir operation not defined: handle = {:?}", + handle + ); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } } diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index e59e8c92..5309e641 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -172,8 +172,9 @@ async fn sftp_server_loop( ch: ChanHandle, ) -> Result<(), Error> { let mut stdio = serv.stdio(ch).await?; - let mut sftp_handler = - SftpHandler::::new(&buffer_in, &mut buffer_out); + let mut file_server = DemoSftpServer::new("user".to_string()); + + let mut sftp_handler = SftpHandler::new(&mut file_server); Ok(loop { let lr = stdio.read(&mut buffer_in).await?; debug!("SFTP <---- received: {:?}", &buffer_in[0..lr]); diff --git a/sftp/src/asyncsftpserver.rs b/sftp/src/asyncsftpserver.rs new file mode 100644 index 00000000..b2bd93e8 --- /dev/null +++ b/sftp/src/asyncsftpserver.rs @@ -0,0 +1,87 @@ +use crate::proto::{Attrs, FileHandle, Name, StatusCode}; + +use core::marker::PhantomData; + +pub type SftpOpResult = core::result::Result; + +/// All trait functions are optional in the SFTP protocol. +/// Some less core operations have a Provided implementation returning +/// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, +/// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. +pub trait AsyncSftpServer { + type Handle<'a>: Into> + TryFrom> + Debug + Copy; + + /// Opens a file or directory for reading/writing + async fn open<'a>( + filename: &str, + attrs: &Attrs, + ) -> SftpOpResult> { + log::error!("SftpServer Open operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + /// Close either a file or directory handle + async fn close<'a>(&mut self, handle: &FileHandle<'a>) -> SftpOpResult<()> { + log::error!("SftpServer Close operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + async fn read<'a>( + handle: &FileHandle<'a>, + offset: u64, + reply: &mut ReadReply<'_, '_>, + ) -> SftpOpResult<()> { + log::error!("SftpServer Read operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + async fn write<'a>( + handle: &FileHandle<'a>, + offset: u64, + buf: &[u8], + ) -> SftpOpResult<()> { + log::error!("SftpServer Write operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + async fn opendir<'a>(&mut self, dir: &str) -> SftpOpResult> { + log::error!("SftpServer OpenDir operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + async fn readdir<'a>( + handle: &FileHandle<'a>, + reply: &mut DirReply<'_, '_>, + ) -> SftpOpResult<()> { + log::error!("SftpServer ReadDir operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + /// Provides the real path of the directory specified + async fn realpath(&mut self, dir: &str) -> SftpOpResult> { + log::error!("SftpServer RealPath operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } +} + +pub struct ReadReply<'g, 'a> { + chan: ChanOut<'g, 'a>, +} + +impl<'g, 'a> ReadReply<'g, 'a> { + pub async fn reply(self, data: &[u8]) {} +} + +pub struct DirReply<'g, 'a> { + chan: ChanOut<'g, 'a>, +} + +impl<'g, 'a> DirReply<'g, 'a> { + pub async fn reply(self, data: &[u8]) {} +} + +// TODO: Implement correct Channel Out +pub struct ChanOut<'g, 'a> { + _phantom_g: PhantomData<&'g ()>, + _phantom_a: PhantomData<&'a ()>, +} diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 1501b72d..5e7d91ba 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,5 +1,4 @@ mod proto; -mod sftperror; mod sftphandle; mod sftpserver; diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 22c0896e..e7e1de76 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -6,7 +6,6 @@ use crate::sftpserver::SftpServer; use sunset::sshwire::{SSHDecode, SSHSink, SSHSource, WireError, WireResult}; -use core::marker::PhantomData; use core::u32; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; @@ -101,26 +100,18 @@ impl<'g> SSHSink for SftpSink<'g> { } } -#[derive(Debug, Clone)] -pub struct SftpHandler<'a, T> -where - T: SftpServer, -{ - server_type: PhantomData, - handle_list: Vec>, +//#[derive(Debug, Clone)] +pub struct SftpHandler<'a> { + file_server: &'a mut dyn SftpServer<'a>, initialized: bool, } -impl<'a, T> SftpHandler<'a, T> -where - T: SftpServer, -{ - pub fn new(buffer_in: &[u8], buffer_out: &mut [u8]) -> Self { - SftpHandler { - server_type: PhantomData, - handle_list: vec![], - initialized: false, - } +impl<'a> SftpHandler<'a> { + pub fn new( + file_server: &'a mut impl SftpServer<'a>, + // max_file_handlers: u32 + ) -> Self { + SftpHandler { file_server, initialized: false } } /// Decodes the buffer_in request, process the request delegating operations to an Struct implementing SftpServer, @@ -148,13 +139,13 @@ where self.process_known_request(&mut sink, request).await?; } Err(e) => match e { - WireError::UnknownPacket { number } => { + WireError::UnknownPacket { number: _ } => { warn!("Error decoding SFTP Packet:{:?}", e); - push_unsuported(ReqId(u32::MAX), &mut sink)?; + push_unsupported(ReqId(u32::MAX), &mut sink)?; } _ => { error!("Error decoding SFTP Packet: {:?}", e); - push_unsuported(ReqId(u32::MAX), &mut sink)?; + push_unsupported(ReqId(u32::MAX), &mut sink)?; } }, }; @@ -166,10 +157,7 @@ where &mut self, sink: &mut SftpSink<'_>, request: SftpPacket<'_>, - ) -> Result<(), WireError> - where - T: SftpServer, - { + ) -> Result<(), WireError> { if !self.initialized && !matches!(request, SftpPacket::Init(_)) { push_general_failure(ReqId(u32::MAX), "Not Initialized", sink)?; error!("Request sent before init: {:?}", request); @@ -189,20 +177,19 @@ where } SftpPacket::PathInfo(req_id, path_info) => { let a_name = - T::realpath(path_info.path.as_str().expect( - "Could not deref and the errors are not harmonised", - )) - .await - .expect("Could not deref and the errors are not harmonised"); + self.file_server + .realpath(path_info.path.as_str().expect( + "Could not deref and the errors are not harmonized", + )) + .expect("Could not deref and the errors are not harmonized"); let response = SftpPacket::Name(req_id, a_name); response.encode_response(sink)?; } SftpPacket::Open(req_id, open) => { - match T::open(open.filename.as_str()?, &open.attrs).await { + match self.file_server.open(open.filename.as_str()?, &open.attrs) { Ok(handle) => { - self.handle_list.push(handle.into()); let response = SftpPacket::Handle( req_id, proto::Handle { handle: handle.into() }, @@ -211,29 +198,61 @@ where info!("Sending '{:?}'", response); } Err(status_code) => { - let response = SftpPacket::Status( - req_id, - Status { - code: status_code, - message: "".into(), - lang: "EN".into(), - }, - ); - response.encode_response(sink)?; - info!("Sending '{:?}'", response); + error!("Open failed: {:?}", status_code); + push_general_failure(req_id, "", sink)?; + } + }; + } + SftpPacket::Write(req_id, write) => { + match self.file_server.write( + &write.handle, + write.offset, + write.data.as_ref(), + ) { + Ok(_) => push_ok(req_id, sink)?, + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure(req_id, "error writing ", sink)? } }; - // push_unsuported(req_id, sink)?; + } + SftpPacket::Close(req_id, close) => { + match self.file_server.close(&close.handle) { + Ok(_) => push_ok(req_id, sink)?, + Err(e) => { + error!("SFTP Close thrown: {:?}", e); + push_general_failure(req_id, "", sink)? + } + } } _ => { - push_unsuported(ReqId(0), sink)?; + push_unsupported(ReqId(0), sink)?; } }; Ok(()) } } -fn push_unsuported(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireError> { +#[inline] +fn push_ok(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireError> { + let response = SftpPacket::Status( + req_id, + Status { + code: StatusCode::SSH_FX_OK, + message: "".into(), + lang: "EN".into(), + }, + ); + debug!("Pushing an OK status message: {:?}", response); + response.encode_response(sink)?; + Ok(()) +} + +#[inline] +fn push_unsupported( + req_id: ReqId, + sink: &mut SftpSink<'_>, +) -> Result<(), WireError> { let response = SftpPacket::Status( req_id, Status { @@ -247,6 +266,7 @@ fn push_unsuported(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireErr Ok(()) } +#[inline] fn push_general_failure( req_id: ReqId, msg: &'static str, diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index c669dcdd..4c7f1fbf 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,6 +1,5 @@ use crate::proto::{Attrs, FileHandle, Name, StatusCode}; -use core::fmt::Debug; use core::marker::PhantomData; pub type SftpOpResult = core::result::Result; @@ -9,62 +8,79 @@ pub type SftpOpResult = core::result::Result; /// Some less core operations have a Provided implementation returning /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. -pub trait SftpServer { - type Handle<'a>: Into> + TryFrom> + Debug + Copy; +pub trait SftpServer<'a> { + // type Handle<'a>: Into> + TryFrom> + Debug + Copy; /// Opens a file or directory for reading/writing - async fn open<'a>( + fn open( &mut self, filename: &str, attrs: &Attrs, - ) -> SftpOpResult> { - log::error!("SftpServer Open operation not defined"); + ) -> SftpOpResult> { + log::error!( + "SftpServer Open operation not defined: filename = {:?}, attrs = {:?}", + filename, + attrs + ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } /// Close either a file or directory handle - async fn close<'a>(&mut self, handle: &Self::Handle<'a>) -> SftpOpResult<()> { - log::error!("SftpServer Close operation not defined"); + fn close(&mut self, handle: &FileHandle) -> SftpOpResult<()> { + log::error!("SftpServer Close operation not defined: handle = {:?}", handle); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - async fn read<'a>( + fn read( &mut self, - handle: &Self::Handle<'a>, + handle: &FileHandle, offset: u64, - reply: &mut ReadReply, + reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { - log::error!("SftpServer Read operation not defined"); + log::error!( + "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", + handle, + offset + ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - async fn write<'a>( + fn write( &mut self, - handle: &Self::Handle<'a>, + handle: &FileHandle, offset: u64, buf: &[u8], ) -> SftpOpResult<()> { - log::error!("SftpServer Write operation not defined"); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + log::error!( + "SftpServer Write operation: handle = {:?}, offset = {:?}, buf = {:?}", + handle, + offset, + String::from_utf8(buf.to_vec()) + ); + Ok(()) } - async fn opendir<'a>(&mut self, dir: &str) -> SftpOpResult> { - log::error!("SftpServer OpenDir operation not defined"); + fn opendir(&mut self, dir: &str) -> SftpOpResult> { + log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - async fn readdir<'a>( + fn readdir( &mut self, - handle: &Self::Handle<'a>, - reply: &mut DirReply, + handle: &FileHandle, + reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { - log::error!("SftpServer ReadDir operation not defined"); + log::error!( + "SftpServer ReadDir operation not defined: handle = {:?}", + handle + ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } /// Provides the real path of the directory specified - async fn realpath(&mut self, dir: &str) -> SftpOpResult> { - log::error!("SftpServer RealPath operation not defined"); + fn realpath(&mut self, dir: &str) -> SftpOpResult> { + log::error!("SftpServer RealPath operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } } @@ -74,7 +90,7 @@ pub struct ReadReply<'g, 'a> { } impl<'g, 'a> ReadReply<'g, 'a> { - pub async fn reply(self, data: &[u8]) {} + pub fn reply(self, data: &[u8]) {} } pub struct DirReply<'g, 'a> { @@ -82,7 +98,7 @@ pub struct DirReply<'g, 'a> { } impl<'g, 'a> DirReply<'g, 'a> { - pub async fn reply(self, data: &[u8]) {} + pub fn reply(self, data: &[u8]) {} } // TODO: Implement correct Channel Out From 58ddd6de639c20cb528e59b4f155877096a4c2d7 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 10 Sep 2025 11:06:34 +1000 Subject: [PATCH 092/393] Simple random data files generation for big write testing --- .../sftp/std/testing/test_only_file_upload.sh | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100755 demo/sftp/std/testing/test_only_file_upload.sh diff --git a/demo/sftp/std/testing/test_only_file_upload.sh b/demo/sftp/std/testing/test_only_file_upload.sh new file mode 100755 index 00000000..eec48750 --- /dev/null +++ b/demo/sftp/std/testing/test_only_file_upload.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Generate random data files +echo "Generating random data files..." +dd if=/dev/random bs=128 count=1 of=./128B_random 2>/dev/null +dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null +dd if=/dev/random bs=512 count=4 of=./2kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=1024 of=./1MB_random 2>/dev/null + + +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." +echo "Test Results:" +echo "=============" + +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +put ./128B_random +put ./512B_random +put ./2kB_random +put ./1MB_random +bye +EOF + + +if [ $? -eq 0 ]; then + echo "PASS" +else + echo "FAIL" +fi + +echo "Cleaning up local files..." +rm -f ./*_random + +echo "Upload test completed." \ No newline at end of file From ec17dde517d7087ba36baf48e0176bedc64e2fa5 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 10 Sep 2025 16:40:02 +1000 Subject: [PATCH 093/393] Adding SFTP Packet Header and SSH_FXP_WRITE offset definitions --- sftp/src/proto.rs | 47 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 65125ca6..27eb6a64 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -8,6 +8,30 @@ use sunset_sshwire_derive::{SSHDecode, SSHEncode}; use log::{debug, error, info, log, trace, warn}; use num_enum::FromPrimitive; use paste::paste; + +/// SFTP Minimum packet length is 9 bytes corresponding with `SSH_FXP_INIT` +pub const SFTP_MINIMUM_PACKET_LEN: usize = 9; + +/// SFTP packets have the packet type after a u32 length field +pub const SFTP_FIELD_ID_INDEX: usize = 4; +/// SFTP packets ID length is 1 byte +pub const SFTP_FIELD_ID_LEN: usize = 1; +/// SFTP packets start with the length field +pub const SFTP_FIELD_LEN_INDEX: usize = 0; +/// SFTP packets length field us u32 +pub const SFTP_FIELD_LEN_LENGTH: usize = 4; + +// SSH_FXP_WRITE SFTP Packet definition used to decode long packets that do not fit in one buffer + +/// SFTP SSH_FXP_WRITE Packet cannot be shorter than this (len:4+pnum:1+rid:4+hand:4+0+data:4+0 bytes = 17 bytes) [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) +pub const SFTP_MINIMUM_WRITE_PACKET_LEN: usize = 17; + +/// SFTP SSH_FXP_WRITE Packet request id field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) +pub const SFTP_WRITE_REQID_INDEX: usize = 5; + +/// SFTP SSH_FXP_WRITE Packet handle field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) +pub const SFTP_WRITE_HANDLE_INDEX: usize = 9; + // TODO is utf8 enough, or does this need to be an opaque binstring? #[derive(Debug, SSHEncode, SSHDecode)] pub struct Filename<'a>(TextString<'a>); @@ -590,15 +614,22 @@ macro_rules! sftpmessages { 'de: 'a { - let sftp_packet = Self::dec(s)? ; - - if (!sftp_packet.sftp_num().is_request() - && !sftp_packet.sftp_num().is_init()) - { - return Err(WireError::PacketWrong) + // let sftp_packet = Self::dec(s)?; + match Self::dec(s) { + Ok(sftp_packet)=> { + if (!sftp_packet.sftp_num().is_request() + && !sftp_packet.sftp_num().is_init()) + { + Err(WireError::PacketWrong) + }else{ + Ok(sftp_packet) + + } + }, + Err(e) => { + Err(e) + } } - - Ok(sftp_packet) } /// Encode a response. From 76346feec4ba30c491894073b433f91e7fd16070 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 10 Sep 2025 16:41:09 +1000 Subject: [PATCH 094/393] Simplified Main Demo SFTP loop and reduced the SFTP buffers size --- demo/sftp/std/src/main.rs | 54 +++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 5309e641..c6e3ebf9 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -144,10 +144,33 @@ impl DemoServer for StdDemo { info!("SFTP loop has received a channel handle {:?}", ch.num()); - let buffer_in = [0u8; 1000]; - let buffer_out = [0u8; 1000]; + let mut buffer_in = [0u8; 512]; + let mut buffer_out = [0u8; 512]; + + match { + let mut stdio = serv.stdio(ch).await?; + let mut file_server = DemoSftpServer::new("user".to_string()); + + let mut sftp_handler = + SftpHandler::new(&mut file_server, buffer_in.len()); + loop { + let lr = stdio.read(&mut buffer_in).await?; + trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); + if lr == 0 { + debug!("client disconnected"); + break; + } - match sftp_server_loop(serv, buffer_in, buffer_out, ch).await { + let lw = sftp_handler + .process(&buffer_in[0..lr], &mut buffer_out) + .await?; + if lw > 0 { + stdio.write(&mut buffer_out[0..lw]).await?; + trace!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); + } + } + Ok::<_, Error>(()) + } { Ok(_) => { warn!("sftp server loop finished gracefully"); } @@ -165,31 +188,6 @@ impl DemoServer for StdDemo { } } -async fn sftp_server_loop( - serv: &SSHServer<'_>, - mut buffer_in: [u8; 1000], - mut buffer_out: [u8; 1000], - ch: ChanHandle, -) -> Result<(), Error> { - let mut stdio = serv.stdio(ch).await?; - let mut file_server = DemoSftpServer::new("user".to_string()); - - let mut sftp_handler = SftpHandler::new(&mut file_server); - Ok(loop { - let lr = stdio.read(&mut buffer_in).await?; - debug!("SFTP <---- received: {:?}", &buffer_in[0..lr]); - if lr == 0 { - debug!("client disconnected"); - return Ok(()); - } - - let lw = sftp_handler.process(&buffer_in[0..lr], &mut buffer_out).await?; - - stdio.write(&mut buffer_out[0..lw]).await?; - debug!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); - }) -} - // TODO: pool_size should be NUM_LISTENERS but needs a literal #[embassy_executor::task(pool_size = 4)] async fn listen( From 4a6057ea58d5774de0e8bfd12f2443b78fb0a209 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 10 Sep 2025 16:44:47 +1000 Subject: [PATCH 095/393] WIP: Working on long WRITE packets processing --- sftp/src/sftphandle.rs | 159 +++++++++++++++++++++++++++++++++++------ 1 file changed, 137 insertions(+), 22 deletions(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index e7e1de76..2659b25b 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,6 +1,7 @@ use crate::proto::{ - self, FileHandle, InitVersionLowest, ReqId, SFTP_VERSION, SftpPacket, Status, - StatusCode, + self, Handle, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, + SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, SFTP_MINIMUM_PACKET_LEN, + SFTP_VERSION, SFTP_WRITE_REQID_INDEX, SftpNum, SftpPacket, Status, StatusCode, }; use crate::sftpserver::SftpServer; @@ -9,7 +10,10 @@ use sunset::sshwire::{SSHDecode, SSHSink, SSHSource, WireError, WireResult}; use core::u32; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; +use std::usize; +/// This implementation is an extension of the SSHSource interface to handle some challenges with SFTP packets +/// #[derive(Default, Debug)] pub struct SftpSource<'de> { pub buffer: &'de [u8], @@ -19,7 +23,7 @@ pub struct SftpSource<'de> { impl<'de> SSHSource<'de> for SftpSource<'de> { fn take(&mut self, len: usize) -> sunset::sshwire::WireResult<&'de [u8]> { if len + self.index > self.buffer.len() { - return Err(WireError::NoRoom); + return Err(WireError::RanOut); } let original_index = self.index; let slice = &self.buffer[self.index..self.index + len]; @@ -42,15 +46,80 @@ impl<'de> SSHSource<'de> for SftpSource<'de> { } } -// // This implementation is an extension of the SSHSource interface. -// impl<'de> SftpSource<'de> { -// /// Rewinds the index back to the initial byte -// /// -// /// In case of an error deserialising the SSHSource it allows reprocesing the buffer from start -// pub fn rewind(&mut self) -> () { -// self.index = 0; -// } -// } +impl<'de> SftpSource<'de> { + pub fn new(buffer: &'de [u8]) -> Self { + SftpSource { buffer: buffer, index: 0 } + } + + /// Rewinds the index back to the initial byte + /// + /// In case of an error deserializing the SSHSource it allows reprocessing the buffer from start + pub fn rewind(&mut self) -> () { + self.index = 0; + } + + /// Peaks the buffer for packet type. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: will only work in well formed packets, in other case the result will contain garbage + fn peak_packet_type(&self) -> WireResult { + // const SFTP_ID_BUFFER_INDEX: usize = 4; // All SFTP packet have the packet type after a u32 length field + // const SFTP_MINIMUM_LENGTH: usize = 9; // Corresponds to a minimal SSH_FXP_INIT packet + if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + Err(WireError::PacketWrong) + } else { + Ok(SftpNum::from(self.buffer[SFTP_FIELD_ID_INDEX])) + } + } + + /// Peaks the buffer for packet length. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: will only work in well formed packets, in other case the result will contain garbage + fn peak_packet_len(&self) -> WireResult { + if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + Err(WireError::PacketWrong) + } else { + let mut raw_bytes = [0u8; 4]; + raw_bytes.copy_from_slice( + &self.buffer[SFTP_FIELD_LEN_INDEX + ..SFTP_FIELD_LEN_INDEX + SFTP_FIELD_LEN_LENGTH], + ); + + Ok(u32::from_be_bytes(raw_bytes)) + } + } + + /// Assuming that the buffer contains a Write request packet, Peaks the buffer for the handle length. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: will only work in well formed write packets, in other case the result will contain garbage + fn peak_write_handle_offset_n_data_len( + &mut self, + ) -> WireResult<(Handle<'_>, ReqId, u64, u32)> { + if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + Err(WireError::PacketWrong) + } else { + let prev_index = self.index; + self.index = SFTP_WRITE_REQID_INDEX; + let req_id = ReqId::dec(self)?; + let handle = Handle::dec(self)?; + let offset = u64::dec(self)?; + let data_len = u32::dec(self)?; + + self.index = prev_index; + + debug!( + "Request ID = {:?}, Handle = {:?}, offset = {:?}, data length = {:?}, ", + req_id, handle, offset, data_len + ); + Ok((handle, req_id, offset, data_len)) + } + } +} #[derive(Default)] pub struct SftpSink<'g> { @@ -59,21 +128,19 @@ pub struct SftpSink<'g> { } impl<'g> SftpSink<'g> { - const LENG_FIELD_LEN: usize = 4; // TODO: Move it to a better location - pub fn new(s: &'g mut [u8]) -> Self { - SftpSink { buffer: s, index: Self::LENG_FIELD_LEN } + SftpSink { buffer: s, index: SFTP_FIELD_LEN_LENGTH } } /// Finalise the buffer by prepending the payload size and returning /// /// Returns the final index in the buffer as a reference for the space used pub fn finalise(&mut self) -> usize { - if self.index <= SftpSink::LENG_FIELD_LEN { + if self.index <= SFTP_FIELD_LEN_LENGTH { warn!("SftpSink trying to terminate it before pushing data"); return 0; } // size is 0 - let used_size = (self.index - 4) as u32; + let used_size = (self.index - SFTP_FIELD_LEN_LENGTH) as u32; used_size .to_be_bytes() @@ -103,15 +170,27 @@ impl<'g> SSHSink for SftpSink<'g> { //#[derive(Debug, Clone)] pub struct SftpHandler<'a> { file_server: &'a mut dyn SftpServer<'a>, + buffer_in_len: usize, initialized: bool, + long_packet: bool, } impl<'a> SftpHandler<'a> { pub fn new( file_server: &'a mut impl SftpServer<'a>, - // max_file_handlers: u32 + buffer_len: usize, // max_file_handlers: u32 ) -> Self { - SftpHandler { file_server, initialized: false } + if buffer_len < 256 { + warn!( + "Buffer length too small, must be at least 256 bytes. You are in uncharted territory" + ) + } + SftpHandler { + file_server, + buffer_in_len: buffer_len, + long_packet: false, + initialized: false, + } } /// Decodes the buffer_in request, process the request delegating operations to an Struct implementing SftpServer, @@ -121,15 +200,17 @@ impl<'a> SftpHandler<'a> { buffer_in: &[u8], buffer_out: &mut [u8], ) -> WireResult { - if buffer_in.len() < 4 { + let in_len = buffer_in.len(); + debug!("Received {:} bytes to process", in_len); + if !self.long_packet & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) { return Err(WireError::PacketWrong); } - let mut source = SftpSource { buffer: buffer_in, index: 0 }; + let mut source = SftpSource::new(buffer_in); trace!("Source content: {:?}", source); let packet_length = u32::dec(&mut source)?; - trace!("Packet field lenght content: {}", packet_length); + trace!("Packet field length content: {}", packet_length); let mut sink = SftpSink::new(buffer_out); @@ -139,6 +220,40 @@ impl<'a> SftpHandler<'a> { self.process_known_request(&mut sink, request).await?; } Err(e) => match e { + WireError::RanOut => { + warn!( + "RanOut for the SFTP Packet in the source buffer: {:?}", + e + ); + source.rewind(); // Not strictly required + let packet_total_length = source.peak_packet_len()?; + let packet_type = source.peak_packet_type()?; + match packet_type { + SftpNum::SSH_FXP_WRITE => { + self.long_packet = true; + let (file_handle, req_id, offset, data_len) = + source.peak_write_handle_offset_n_data_len()?; + warn!( + "We got a long Write packet. Excellent! total len = {:?}, type = {:?}, req_id = {:?}, handle = {:?}, offset = {:?}, data_len = {:?}", + packet_total_length, + packet_type, + req_id, + file_handle, + offset, + data_len + ); + } + _ => { + error!( + "We do not know how to handle this long packet: {:?}", + packet_type + ); + todo!( + " Push a general failure with the request ID and RanOut comment" + ); + } + }; + } WireError::UnknownPacket { number: _ } => { warn!("Error decoding SFTP Packet:{:?}", e); push_unsupported(ReqId(u32::MAX), &mut sink)?; From c2115024679c37e308f28579d3254275cffadf5e Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 12 Sep 2025 13:46:26 +1000 Subject: [PATCH 096/393] WIP: Processing long Write Requests For now the flow of receiving SFTP Write request longer than the buffer is going in the right direction. I have been able to process up to 64kB of data. Still this is not complete since I need to: - Write the data received and check that the data sent and received has not been altered - Issue: Writes with files over 64kB fail - Reshape the file handle exchanged from the demoserver so it has a fixed length and it is compact - Refactor sftphandle --- demo/sftp/std/src/demosftpserver.rs | 2 +- demo/sftp/std/src/main.rs | 1 - .../sftp/std/testing/test_only_file_upload.sh | 18 +- sftp/src/proto.rs | 3 +- sftp/src/sftphandle.rs | 310 +++++++++++++----- sftp/src/sftpserver.rs | 6 + 6 files changed, 254 insertions(+), 86 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index bb846ab9..f1ad124e 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -8,7 +8,7 @@ use sunset_sftp::{ use log::{debug, error, info, log, trace, warn}; pub struct DemoSftpServer { - valid_handlers: Vec, + valid_handlers: Vec, // TODO: Obscure the handlers user_path: String, } diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index c6e3ebf9..8d98977c 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -36,7 +36,6 @@ async fn net_task(mut runner: embassy_net::Runner<'static, TunTapDevice>) -> ! { #[embassy_executor::task] async fn main_task(spawner: Spawner) { - // TODO config let opt_tap0 = "tap0"; let ip4 = "192.168.69.2"; let cir = 24; diff --git a/demo/sftp/std/testing/test_only_file_upload.sh b/demo/sftp/std/testing/test_only_file_upload.sh index eec48750..97f93269 100755 --- a/demo/sftp/std/testing/test_only_file_upload.sh +++ b/demo/sftp/std/testing/test_only_file_upload.sh @@ -8,7 +8,11 @@ REMOTE_USER="any" echo "Generating random data files..." dd if=/dev/random bs=128 count=1 of=./128B_random 2>/dev/null dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null -dd if=/dev/random bs=512 count=4 of=./2kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=2 of=./2kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=4 of=./4kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null # OK +dd if=/dev/random bs=1024 count=65 of=./MaxMaybe_random 2>/dev/null # Fails dd if=/dev/random bs=1024 count=1024 of=./1MB_random 2>/dev/null @@ -16,11 +20,15 @@ echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." echo "Test Results:" echo "=============" +# put ./128B_random +# put ./512B_random +# put ./2kB_random +# put ./4kB_random +# put ./16kB_random +# put ./1MB_random sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF -put ./128B_random -put ./512B_random -put ./2kB_random -put ./1MB_random +put ./64kB_random +put ./MaxMaybe_random bye EOF diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 27eb6a64..17d21a96 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -593,8 +593,7 @@ macro_rules! sftpmessages { if !num.is_response() { return Err(WireError::PacketWrong) // return error::SSHProto.fail(); - // TODO: Not an error in the SSHProtocol rather the SFTP. - // Maybe is time to define an SftpError + // TODO: Not an error in the SSHProtocol rather the SFTP Protocol. } let id = ReqId(u32::dec(s)?); diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 2659b25b..42ce44ec 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,19 +1,63 @@ +use crate::FileHandle; use crate::proto::{ - self, Handle, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, - SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, SFTP_MINIMUM_PACKET_LEN, - SFTP_VERSION, SFTP_WRITE_REQID_INDEX, SftpNum, SftpPacket, Status, StatusCode, + self, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, + SFTP_FIELD_LEN_LENGTH, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, + SFTP_WRITE_REQID_INDEX, SftpNum, SftpPacket, Status, StatusCode, }; -use crate::sftpserver::SftpServer; +use crate::sftpserver::{FILE_HANDLE_MAX_LEN, SftpServer}; -use sunset::sshwire::{SSHDecode, SSHSink, SSHSource, WireError, WireResult}; +use sunset::sshwire::{ + BinString, SSHDecode, SSHSink, SSHSource, WireError, WireResult, +}; use core::u32; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; use std::usize; -/// This implementation is an extension of the SSHSource interface to handle some challenges with SFTP packets -/// +/// Used to keep record of a long SFTP Write request that does not fit in receiving buffer and requires processing in batches +#[derive(Debug)] +struct PartialWriteRequestTracker { + req_id: ReqId, + file_handle: [u8; FILE_HANDLE_MAX_LEN], // TODO: Change the file handle in SftpServer functions signature so it has a sort fixed length handle. + file_handle_len: usize, + data_len: u32, + remain_data_len: u32, + remain_data_offset: u64, +} + +impl PartialWriteRequestTracker { + pub fn new( + req_id: ReqId, + file_handle: &FileHandle<'_>, + data_len: u32, + remain_data_len: u32, + remain_data_offset: u64, + ) -> WireResult { + let handle_data = file_handle.0.as_ref(); + if handle_data.len() > FILE_HANDLE_MAX_LEN { + return Err(WireError::PacketWrong); // Handle too large + } + let mut ret = PartialWriteRequestTracker { + req_id, + file_handle: [0u8; FILE_HANDLE_MAX_LEN], + file_handle_len: handle_data.len(), + data_len, + remain_data_len, + remain_data_offset, + }; + ret.file_handle[..ret.file_handle_len] + .copy_from_slice(file_handle.0.as_ref()); + + Ok(ret) + } + + pub fn get_file_handle(&self) -> FileHandle<'_> { + FileHandle(BinString(&self.file_handle[..self.file_handle_len])) + } +} + +/// SftpSource implements SSHSource and also extra functions to handle some challenges with long SFTP packets in constrained environments #[derive(Default, Debug)] pub struct SftpSource<'de> { pub buffer: &'de [u8], @@ -21,6 +65,7 @@ pub struct SftpSource<'de> { } impl<'de> SSHSource<'de> for SftpSource<'de> { + // Original take fn take(&mut self, len: usize) -> sunset::sshwire::WireResult<&'de [u8]> { if len + self.index > self.buffer.len() { return Err(WireError::RanOut); @@ -40,9 +85,7 @@ impl<'de> SSHSource<'de> for SftpSource<'de> { } fn ctx(&mut self) -> &mut sunset::packets::ParseContext { - todo!( - "I don't know what to do with the ctx, since sftp does not have context" - ); + todo!("Which context for sftp?"); } } @@ -51,13 +94,6 @@ impl<'de> SftpSource<'de> { SftpSource { buffer: buffer, index: 0 } } - /// Rewinds the index back to the initial byte - /// - /// In case of an error deserializing the SSHSource it allows reprocessing the buffer from start - pub fn rewind(&mut self) -> () { - self.index = 0; - } - /// Peaks the buffer for packet type. This does not advance the reading index /// /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail @@ -92,33 +128,60 @@ impl<'de> SftpSource<'de> { } } - /// Assuming that the buffer contains a Write request packet, Peaks the buffer for the handle length. This does not advance the reading index + /// Assuming that the buffer contains a Write request packet initial bytes, Peaks the buffer for the handle length. This does not advance the reading index /// /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail /// /// **Warning**: will only work in well formed write packets, in other case the result will contain garbage - fn peak_write_handle_offset_n_data_len( + fn get_packet_partial_write_content_and_tracker( &mut self, - ) -> WireResult<(Handle<'_>, ReqId, u64, u32)> { + ) -> WireResult<( + FileHandle<'de>, + ReqId, + u64, + BinString<'de>, + PartialWriteRequestTracker, + )> { if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { Err(WireError::PacketWrong) } else { let prev_index = self.index; self.index = SFTP_WRITE_REQID_INDEX; let req_id = ReqId::dec(self)?; - let handle = Handle::dec(self)?; + let file_handle = FileHandle::dec(self)?; let offset = u64::dec(self)?; let data_len = u32::dec(self)?; + let data_len_in_buffer = self.buffer.len() - self.index; + let data_in_buffer = BinString(self.take(data_len_in_buffer)?); + self.index = prev_index; - debug!( - "Request ID = {:?}, Handle = {:?}, offset = {:?}, data length = {:?}, ", - req_id, handle, offset, data_len + let remain_data_len = data_len - data_len_in_buffer as u32; + let remain_data_offset = offset + data_len_in_buffer as u64; + trace!( + "Request ID = {:?}, Handle = {:?}, offset = {:?}, data length in buffer = {:?}, data in current buffer {:?} ", + req_id, file_handle, offset, data_len_in_buffer, data_in_buffer ); - Ok((handle, req_id, offset, data_len)) + + let write_tracker = PartialWriteRequestTracker::new( + req_id, + &file_handle, + data_len, + remain_data_len, + remain_data_offset, + )?; + + Ok((file_handle, req_id, offset, data_in_buffer, write_tracker)) } } + + /// Used to decode the whole SSHSource as a single BinString + /// + /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. + fn dec_all_as_binstring(&mut self) -> WireResult> { + Ok(BinString(self.take(self.buffer.len())?)) + } } #[derive(Default)] @@ -135,7 +198,7 @@ impl<'g> SftpSink<'g> { /// Finalise the buffer by prepending the payload size and returning /// /// Returns the final index in the buffer as a reference for the space used - pub fn finalise(&mut self) -> usize { + pub fn finalize(&mut self) -> usize { if self.index <= SFTP_FIELD_LEN_LENGTH { warn!("SftpSink trying to terminate it before pushing data"); return 0; @@ -170,9 +233,14 @@ impl<'g> SSHSink for SftpSink<'g> { //#[derive(Debug, Clone)] pub struct SftpHandler<'a> { file_server: &'a mut dyn SftpServer<'a>, + /// buffer_in_len: usize, + /// Once the client and the server have verified the agreed SFTP version the session is initialized initialized: bool, - long_packet: bool, + // /// Use to process SFTP packets that have been received partially and the remaining is expected in successive buffers + // long_packet: bool, + /// Use to process SFTP Write packets that have been received partially and the remaining is expected in successive buffers + partial_write_request_tracker: Option, } impl<'a> SftpHandler<'a> { @@ -188,8 +256,9 @@ impl<'a> SftpHandler<'a> { SftpHandler { file_server, buffer_in_len: buffer_len, - long_packet: false, + // long_packet: false, initialized: false, + partial_write_request_tracker: None, } } @@ -202,70 +271,157 @@ impl<'a> SftpHandler<'a> { ) -> WireResult { let in_len = buffer_in.len(); debug!("Received {:} bytes to process", in_len); - if !self.long_packet & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) { - return Err(WireError::PacketWrong); - } let mut source = SftpSource::new(buffer_in); trace!("Source content: {:?}", source); + let mut sink = SftpSink::new(buffer_out); + + if let Some(mut write_tracker) = self.partial_write_request_tracker.take() { + trace!( + "Processing successive chunks of a long write packet . Stored data: {:?}", + write_tracker + ); + if in_len > write_tracker.remain_data_len as usize { + error!( + "There is too much data in the buffer! {:?} > than max expected {:?}", + in_len, write_tracker.remain_data_len + ); + return Err(WireError::PacketWrong); // TODO: Handle this error instead of failing + } + + let current_write_offset = write_tracker.remain_data_offset; + let data_in_buffer = source.dec_all_as_binstring()?; + + // TODO: Do proper casting with checks u32::try_from(data_in_buffer.0.len()) + let data_in_buffer_len = data_in_buffer.0.len() as u32; + + write_tracker.remain_data_offset += data_in_buffer_len as u64; + write_tracker.remain_data_len -= data_in_buffer_len; + + let file_handle = write_tracker.get_file_handle(); + debug!( + "Processing successive chunks of a long write packet. Writing : file_handle = {:?}, write_offset = {:?}, data = {:?}, data remaining = {:?}", + file_handle, + current_write_offset, + data_in_buffer, + write_tracker.remain_data_len + ); + match self.file_server.write( + &file_handle, + current_write_offset, + data_in_buffer.as_ref(), + ) { + Ok(_) => { + if write_tracker.remain_data_len > 0 { + self.partial_write_request_tracker = Some(write_tracker); + } else { + push_ok(write_tracker.req_id, &mut sink)?; + info!("Finished multi part Write Request"); + self.partial_write_request_tracker = None; // redundant + } + } + Err(e) => { + self.partial_write_request_tracker = None; + error!("SFTP write thrown: {:?}", e); + push_general_failure( + write_tracker.req_id, + "error writing", + &mut sink, + )?; + } + }; + + return Ok(sink.finalize()); + } + + if self.partial_write_request_tracker.is_none() + & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) + { + return Err(WireError::PacketWrong); + } + let packet_length = u32::dec(&mut source)?; trace!("Packet field length content: {}", packet_length); - let mut sink = SftpSink::new(buffer_out); - match SftpPacket::decode_request(&mut source) { Ok(request) => { info!("received request: {:?}", request); self.process_known_request(&mut sink, request).await?; } - Err(e) => match e { - WireError::RanOut => { - warn!( - "RanOut for the SFTP Packet in the source buffer: {:?}", - e - ); - source.rewind(); // Not strictly required - let packet_total_length = source.peak_packet_len()?; - let packet_type = source.peak_packet_type()?; - match packet_type { - SftpNum::SSH_FXP_WRITE => { - self.long_packet = true; - let (file_handle, req_id, offset, data_len) = - source.peak_write_handle_offset_n_data_len()?; - warn!( - "We got a long Write packet. Excellent! total len = {:?}, type = {:?}, req_id = {:?}, handle = {:?}, offset = {:?}, data_len = {:?}", - packet_total_length, - packet_type, - req_id, - file_handle, - offset, - data_len - ); - } - _ => { - error!( - "We do not know how to handle this long packet: {:?}", - packet_type - ); - todo!( - " Push a general failure with the request ID and RanOut comment" - ); - } - }; - } - WireError::UnknownPacket { number: _ } => { - warn!("Error decoding SFTP Packet:{:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; - } - _ => { - error!("Error decoding SFTP Packet: {:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; + Err(e) => { + match e { + WireError::RanOut => { + warn!( + "RanOut for the SFTP Packet in the source buffer: {:?}", + e + ); + // let packet_total_length = source.peak_packet_len()?; + let packet_type = source.peak_packet_type()?; + match packet_type { + SftpNum::SSH_FXP_WRITE => { + let ( + file_handle, + req_id, + offset, + data_in_buffer, + write_tracker, + ) = source + .get_packet_partial_write_content_and_tracker( + )?; + debug!( + "Packet is too long for the source buffer, will write what we have now and continue writing later" + ); + trace!( + "handle = {:?}, req_id = {:?}, offset = {:?}, data_in_buffer = {:?}, write_tracker = {:?}", + file_handle, + req_id, + offset, + data_in_buffer, + write_tracker + ); + + match self.file_server.write( + &file_handle, + offset, + data_in_buffer.as_ref(), + ) { + Ok(_) => { + self.partial_write_request_tracker = + Some(write_tracker); + } + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure( + req_id, + "error writing ", + &mut sink, + )?; + } + }; + } + _ => { + push_general_failure( + ReqId(u32::MAX), + "Unsupported Request: Too long", + &mut sink, + ); + } + }; + } + WireError::UnknownPacket { number: _ } => { + warn!("Error decoding SFTP Packet:{:?}", e); + push_unsupported(ReqId(u32::MAX), &mut sink)?; + } + _ => { + error!("Error decoding SFTP Packet: {:?}", e); + push_unsupported(ReqId(u32::MAX), &mut sink)?; + } } - }, + } }; - Ok(sink.finalise()) + Ok(sink.finalize()) } async fn process_known_request( diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 4c7f1fbf..0acbfc85 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -2,6 +2,12 @@ use crate::proto::{Attrs, FileHandle, Name, StatusCode}; use core::marker::PhantomData; +// TODO: enforce it and do checks for the FileHandle so it properly obscure the file path and user. +// Hint: In stateful server this can be done with a hash function and a dictionary +/// Used during storage of file handle data for long SFTP Write requests +/// Must be observed by SftpServer handle implementations +pub const FILE_HANDLE_MAX_LEN: usize = 256; + pub type SftpOpResult = core::result::Result; /// All trait functions are optional in the SFTP protocol. From 68838936ea42c3a134b630bcef92c95917811d2d Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 17 Sep 2025 14:47:17 +1000 Subject: [PATCH 097/393] Adding basic obscuring of handler This forces the SftpServer to use obscured handles that are size constrain. It also introduces the handle manager, that is a helper that the SFTP server implementer can use for dealing with the obscured handle and a private handle, that can contain all the information that the local server requires. --- demo/sftp/std/src/demosftpserver.rs | 130 ++++++----- .../sftp/std/testing/test_only_file_upload.sh | 2 +- sftp/src/lib.rs | 3 + sftp/src/obscured_file_handle.rs | 219 ++++++++++++++++++ sftp/src/sftphandle.rs | 64 ++--- sftp/src/sftpserver.rs | 33 ++- 6 files changed, 349 insertions(+), 102 deletions(-) create mode 100644 sftp/src/obscured_file_handle.rs diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index f1ad124e..737c7766 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -1,22 +1,32 @@ -use sunset::sshwire::BinString; use sunset_sftp::{ - Attrs, DirReply, FileHandle, Filename, Name, NameEntry, ReadReply, SftpOpResult, - SftpServer, StatusCode, + Attrs, DirReply, Filename, HandleManager, Name, NameEntry, ObscuredFileHandle, + PathFinder, ReadReply, SftpOpResult, SftpServer, StatusCode, }; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; +struct PrivateFileHandler { + file_path: String, + permissions: Option, +} + +impl PathFinder for PrivateFileHandler { + fn matches_path(&self, path: &str) -> bool { + self.file_path.as_str().eq_ignore_ascii_case(path) + } +} + pub struct DemoSftpServer { - valid_handlers: Vec, // TODO: Obscure the handlers user_path: String, + handlers_manager: HandleManager, } impl DemoSftpServer { pub fn new(user: String) -> Self { DemoSftpServer { - valid_handlers: vec![], user_path: format!("/{}/", user.clone()), + handlers_manager: HandleManager::new(), } } } @@ -25,18 +35,24 @@ impl SftpServer<'_> for DemoSftpServer { fn open( &mut self, filename: &str, - _attrs: &Attrs, - ) -> SftpOpResult> { - if self.valid_handlers.contains(&filename.to_string()) { + attrs: &Attrs, + ) -> SftpOpResult { + debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); + + if self.handlers_manager.is_open(filename) { warn!("File {:?} already open, won't allow it", filename); return Err(StatusCode::SSH_FX_PERMISSION_DENIED); } - self.valid_handlers.push(filename.to_string()); + let fh = self.handlers_manager.create_handle(PrivateFileHandler { + file_path: filename.into(), + permissions: attrs.permissions, + }); + warn!( + "Filename \"{:?}\" will have the obscured file handle: {:?}", + filename, fh + ); - let fh = FileHandle(BinString( - self.valid_handlers.last().expect("just pushed an element").as_bytes(), - )); Ok(fh) } @@ -57,73 +73,83 @@ impl SftpServer<'_> for DemoSftpServer { }])) } - fn close(&mut self, handle: &FileHandle) -> SftpOpResult<()> { - let initial_count = self.valid_handlers.len(); - if initial_count == 0 { - log::error!( - "SftpServer Close operation with no handles stored: handle = {:?}", - handle + fn close( + &mut self, + obscure_file_handle: &ObscuredFileHandle, + ) -> SftpOpResult<()> { + if let Some(handle) = + self.handlers_manager.remove_handle(obscure_file_handle) + { + debug!( + "SftpServer Close operation on {:?} was successful", + handle.file_path + ); + Ok(()) + } else { + error!( + "SftpServer Close operation on handle {:?} failed", + obscure_file_handle ); - return Err(StatusCode::SSH_FX_FAILURE); + Err(StatusCode::SSH_FX_FAILURE) } + } - let filename = - String::from_utf8(handle.0.as_ref().to_vec()).unwrap_or("".into()); + fn write( + &mut self, + obscured_file_handle: &ObscuredFileHandle, + offset: u64, + buf: &[u8], + ) -> SftpOpResult<()> { + let private_file_handle = self + .handlers_manager + .get_handle_value_as_ref(obscured_file_handle) + .ok_or(StatusCode::SSH_FX_FAILURE)?; - if !self.valid_handlers.contains(&filename) { - log::error!( - "SftpServer Close operation could not match an stored handler: handle = {:?}", - handle - ); - return Err(StatusCode::SSH_FX_FAILURE); - } - self.valid_handlers.retain(|handler| handler.ne(&filename)); - log::debug!("SftpServer Close operation on {:?} was successful", filename); + let permissions_poxit = (private_file_handle + .permissions + .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED))?; + + if (permissions_poxit & 0o222) == 0 { + return Err(StatusCode::SSH_FX_PERMISSION_DENIED); + }; + + log::debug!( + "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buf = {:?}", + obscured_file_handle, + private_file_handle.file_path, + offset, + String::from_utf8(buf.to_vec()) + ); Ok(()) } fn read( &mut self, - handle: &FileHandle, + obscured_file_handle: &ObscuredFileHandle, offset: u64, - reply: &mut ReadReply<'_, '_>, + _reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", - handle, + obscured_file_handle, offset ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - fn write( - &mut self, - handle: &FileHandle, - offset: u64, - buf: &[u8], - ) -> SftpOpResult<()> { - log::debug!( - "SftpServer Write operation: handle = {:?}, offset = {:?}, buf = {:?}", - handle, - offset, - String::from_utf8(buf.to_vec()) - ); - Ok(()) - } - - fn opendir(&mut self, dir: &str) -> SftpOpResult> { + fn opendir(&mut self, dir: &str) -> SftpOpResult { log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } fn readdir( &mut self, - handle: &FileHandle, - reply: &mut DirReply<'_, '_>, + obscured_file_handle: &ObscuredFileHandle, + _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", - handle + obscured_file_handle ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } diff --git a/demo/sftp/std/testing/test_only_file_upload.sh b/demo/sftp/std/testing/test_only_file_upload.sh index 97f93269..92d34e71 100755 --- a/demo/sftp/std/testing/test_only_file_upload.sh +++ b/demo/sftp/std/testing/test_only_file_upload.sh @@ -26,9 +26,9 @@ echo "=============" # put ./4kB_random # put ./16kB_random # put ./1MB_random +# put ./MaxMaybe_random sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF put ./64kB_random -put ./MaxMaybe_random bye EOF diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 5e7d91ba..16a8ba76 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,3 +1,4 @@ +mod obscured_file_handle; mod proto; mod sftphandle; mod sftpserver; @@ -9,6 +10,8 @@ pub use sftpserver::SftpServer; pub use sftphandle::SftpHandler; +pub use obscured_file_handle::{HandleManager, ObscuredFileHandle, PathFinder}; + pub use proto::Attrs; pub use proto::FileHandle; pub use proto::Filename; diff --git a/sftp/src/obscured_file_handle.rs b/sftp/src/obscured_file_handle.rs new file mode 100644 index 00000000..34a678d4 --- /dev/null +++ b/sftp/src/obscured_file_handle.rs @@ -0,0 +1,219 @@ +use crate::FileHandle; + +use sunset::sshwire::{BinString, WireError}; + +use std::collections::HashMap; + +pub const FILE_HANDLE_MAX_LEN: usize = 8; + +/// Obscured file handle using Linear Congruential Generator (LCG) for pseudo-random generation. +/// +/// This struct provides a fixed-length handle that appears random but is deterministic +/// based on the input seed. Uses LCG with constants from Numerical Recipes. +/// +/// # Limitations +/// +/// - **Not cryptographically secure**: Predictable if the algorithm and seed are known +/// - **Limited entropy**: u8 seed provides only 256 different possible handles +/// - **Linear correlations**: Sequential seeds produce statistically correlated outputs +/// - **Reversible**: Given the handle, the original seed can potentially be recovered +/// - **Not suitable for security**: Should only be used for obscuration, not protection +/// +/// # Security Note +/// +/// This is intended for basic handle obscuration in SFTP to prevent casual observation +/// of handle-to-file mappings. It is NOT suitable for cryptographic purposes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ObscuredFileHandle { + data: [u8; FILE_HANDLE_MAX_LEN], +} + +impl ObscuredFileHandle { + /// Generate a pseudo-random handle from a u8 seed using Linear Congruential Generator. + /// + /// Same seed will always produce the same handle. Only 256 different handles + /// are possible due to u8 seed limitation. This is deliberate to imply its limitations + /// + /// TODO: If this library is to be hardened this is a point to address + pub fn new(seed: u8) -> Self { + let mut data = [0u8; FILE_HANDLE_MAX_LEN]; + + // Simple Linear Congruential Generator (LCG) + // Using constants from Numerical Recipes + let mut state = seed as u64; + + for chunk in data.chunks_mut(8) { + // LCG: next = (a * current + c) mod 2^64 + state = state.wrapping_mul(1664525).wrapping_add(1013904223); + + let bytes = state.to_le_bytes(); + let copy_len = chunk.len().min(8); + chunk[..copy_len].copy_from_slice(&bytes[..copy_len]); + } + + Self { data } + } + + /// Get the handle as a byte slice + pub fn as_bytes(&self) -> &[u8] { + &self.data + } + + /// Get the handle as a Vec for wire protocol + pub fn to_vec(&self) -> Vec { + self.data.to_vec() + } + + /// Create from existing bytes (for parsing from wire) + pub fn from_bytes(bytes: &[u8]) -> Option { + if bytes.len() != FILE_HANDLE_MAX_LEN { + return None; + } + + let mut data = [0u8; FILE_HANDLE_MAX_LEN]; + data.copy_from_slice(bytes); + Some(Self { data }) + } + + /// Create from BinString (for parsing from SFTP wire protocol) + pub fn from_binstring(binstring: &BinString<'_>) -> Option { + Self::from_bytes(binstring.0) + } + + /// Create from FileHandle (for parsing from SFTP wire protocol) + pub fn from_filehandle(file_handle: &FileHandle<'_>) -> Option { + Self::from_bytes(file_handle.0.0) + } + + /// Convert to BinString for SFTP wire protocol + pub fn to_binstring(&self) -> BinString<'_> { + BinString(&self.data) + } + + /// Convert to FileHandle for SFTP wire protocol + pub fn to_filehandle(&self) -> FileHandle<'_> { + FileHandle(self.to_binstring()) + } +} + +// Display trait for debugging/logging +impl core::fmt::Display for ObscuredFileHandle { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + for byte in &self.data { + write!(f, "{:02x}", byte)?; + } + Ok(()) + } +} + +// Standard trait implementations for BinString conversion +impl<'a> From<&'a ObscuredFileHandle> for BinString<'a> { + fn from(handle: &'a ObscuredFileHandle) -> Self { + handle.to_binstring() + } +} + +impl<'a> TryFrom<&BinString<'a>> for ObscuredFileHandle { + type Error = WireError; + + fn try_from(binstring: &BinString<'a>) -> Result { + Self::from_binstring(binstring).ok_or(WireError::BadString) + } +} + +impl<'a> TryFrom> for ObscuredFileHandle { + type Error = WireError; + + fn try_from(binstring: BinString<'a>) -> Result { + Self::try_from(&binstring) + } +} + +// Conversions with proto::FileHandle +impl<'a> From<&'a ObscuredFileHandle> for crate::proto::FileHandle<'a> { + fn from(handle: &'a ObscuredFileHandle) -> Self { + crate::proto::FileHandle(handle.into()) + } +} + +impl<'a> TryFrom<&crate::proto::FileHandle<'a>> for ObscuredFileHandle { + type Error = WireError; + + fn try_from( + file_handle: &crate::proto::FileHandle<'a>, + ) -> Result { + Self::try_from(&file_handle.0) + } +} + +impl<'a> TryFrom> for ObscuredFileHandle { + type Error = WireError; + + fn try_from( + file_handle: crate::proto::FileHandle<'a>, + ) -> Result { + Self::try_from(&file_handle) + } +} + +/// Used to standarise finding a path within the HandleManager +pub trait PathFinder { + /// Helper function to find elements stored in the HandleManager that matches the give path + fn matches_path(&self, path: &str) -> bool; +} + +// Example usage structure for managing handles +pub struct HandleManager +where + T: PathFinder, +{ + next_handle_id: u8, + handle_map: HashMap, +} + +impl HandleManager { + pub fn new() -> Self { + Self { next_handle_id: 1, handle_map: HashMap::new() } + } + + pub fn create_handle(&mut self, value: T) -> ObscuredFileHandle { + let handle = ObscuredFileHandle::new(self.next_handle_id); + self.next_handle_id += 1; + + self.handle_map.insert(handle, value); + handle + } + + pub fn get_handle_value_as_ref( + &self, + handle: &ObscuredFileHandle, + ) -> Option<&T> { + self.handle_map.get(handle) + } + + pub fn remove_handle(&mut self, handle: &ObscuredFileHandle) -> Option { + self.handle_map.remove(handle) + } + + pub fn handle_exists(&self, handle: &ObscuredFileHandle) -> bool { + self.handle_map.contains_key(handle) + } + + pub fn is_open(&self, filename: &str) -> bool { + if self.handle_map.is_empty() { + return false; + } + self.handle_map.iter().any(|(_, element)| element.matches_path(filename)) + // TODO: Fix this. We need to be able to find out if the filename has been open. That cannot be done with a general T. Is will need to implement a trait to check that + // true + } +} + +impl Default for HandleManager +where + T: PathFinder, +{ + fn default() -> Self { + Self::new() + } +} diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 42ce44ec..2315c59a 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,10 +1,10 @@ -use crate::FileHandle; use crate::proto::{ self, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SFTP_WRITE_REQID_INDEX, SftpNum, SftpPacket, Status, StatusCode, }; -use crate::sftpserver::{FILE_HANDLE_MAX_LEN, SftpServer}; +use crate::sftpserver::SftpServer; +use crate::{FileHandle, ObscuredFileHandle}; use sunset::sshwire::{ BinString, SSHDecode, SSHSink, SSHSource, WireError, WireResult, @@ -19,8 +19,7 @@ use std::usize; #[derive(Debug)] struct PartialWriteRequestTracker { req_id: ReqId, - file_handle: [u8; FILE_HANDLE_MAX_LEN], // TODO: Change the file handle in SftpServer functions signature so it has a sort fixed length handle. - file_handle_len: usize, + obscure_file_handle: ObscuredFileHandle, // TODO: Change the file handle in SftpServer functions signature so it has a sort fixed length handle. data_len: u32, remain_data_len: u32, remain_data_offset: u64, @@ -29,31 +28,23 @@ struct PartialWriteRequestTracker { impl PartialWriteRequestTracker { pub fn new( req_id: ReqId, - file_handle: &FileHandle<'_>, + obscure_file_handle: ObscuredFileHandle, data_len: u32, remain_data_len: u32, remain_data_offset: u64, ) -> WireResult { - let handle_data = file_handle.0.as_ref(); - if handle_data.len() > FILE_HANDLE_MAX_LEN { - return Err(WireError::PacketWrong); // Handle too large - } let mut ret = PartialWriteRequestTracker { req_id, - file_handle: [0u8; FILE_HANDLE_MAX_LEN], - file_handle_len: handle_data.len(), + obscure_file_handle: obscure_file_handle, data_len, remain_data_len, remain_data_offset, }; - ret.file_handle[..ret.file_handle_len] - .copy_from_slice(file_handle.0.as_ref()); - Ok(ret) } - pub fn get_file_handle(&self) -> FileHandle<'_> { - FileHandle(BinString(&self.file_handle[..self.file_handle_len])) + pub fn get_file_handle(&self) -> ObscuredFileHandle { + self.obscure_file_handle.clone() } } @@ -136,7 +127,7 @@ impl<'de> SftpSource<'de> { fn get_packet_partial_write_content_and_tracker( &mut self, ) -> WireResult<( - FileHandle<'de>, + ObscuredFileHandle, ReqId, u64, BinString<'de>, @@ -149,6 +140,11 @@ impl<'de> SftpSource<'de> { self.index = SFTP_WRITE_REQID_INDEX; let req_id = ReqId::dec(self)?; let file_handle = FileHandle::dec(self)?; + + let obscured_file_handle = + ObscuredFileHandle::from_binstring(&file_handle.0) + .ok_or(WireError::BadString)?; + let offset = u64::dec(self)?; let data_len = u32::dec(self)?; @@ -166,13 +162,14 @@ impl<'de> SftpSource<'de> { let write_tracker = PartialWriteRequestTracker::new( req_id, - &file_handle, + ObscuredFileHandle::from_filehandle(&file_handle) + .ok_or(WireError::BadString)?, data_len, remain_data_len, remain_data_offset, )?; - Ok((file_handle, req_id, offset, data_in_buffer, write_tracker)) + Ok((obscured_file_handle, req_id, offset, data_in_buffer, write_tracker)) } } @@ -283,11 +280,12 @@ impl<'a> SftpHandler<'a> { write_tracker ); if in_len > write_tracker.remain_data_len as usize { + // TODO: Investigate if we are receiving one packet and the beginning of the next one error!( "There is too much data in the buffer! {:?} > than max expected {:?}", in_len, write_tracker.remain_data_len ); - return Err(WireError::PacketWrong); // TODO: Handle this error instead of failing + return Err(WireError::PacketWrong); // TODO: Handle this error instead of failing. } let current_write_offset = write_tracker.remain_data_offset; @@ -299,16 +297,16 @@ impl<'a> SftpHandler<'a> { write_tracker.remain_data_offset += data_in_buffer_len as u64; write_tracker.remain_data_len -= data_in_buffer_len; - let file_handle = write_tracker.get_file_handle(); + let obscure_file_handle = write_tracker.get_file_handle(); debug!( - "Processing successive chunks of a long write packet. Writing : file_handle = {:?}, write_offset = {:?}, data = {:?}, data remaining = {:?}", - file_handle, + "Processing successive chunks of a long write packet. Writing : obscure_file_handle = {:?}, write_offset = {:?}, data = {:?}, data remaining = {:?}", + obscure_file_handle, current_write_offset, data_in_buffer, write_tracker.remain_data_len ); match self.file_server.write( - &file_handle, + &obscure_file_handle, current_write_offset, data_in_buffer.as_ref(), ) { @@ -374,7 +372,7 @@ impl<'a> SftpHandler<'a> { ); trace!( "handle = {:?}, req_id = {:?}, offset = {:?}, data_in_buffer = {:?}, write_tracker = {:?}", - file_handle, + file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle req_id, offset, data_in_buffer, @@ -405,7 +403,7 @@ impl<'a> SftpHandler<'a> { ReqId(u32::MAX), "Unsupported Request: Too long", &mut sink, - ); + )?; } }; } @@ -460,10 +458,12 @@ impl<'a> SftpHandler<'a> { } SftpPacket::Open(req_id, open) => { match self.file_server.open(open.filename.as_str()?, &open.attrs) { - Ok(handle) => { + Ok(obscured_file_handle) => { let response = SftpPacket::Handle( req_id, - proto::Handle { handle: handle.into() }, + proto::Handle { + handle: obscured_file_handle.to_filehandle(), + }, ); response.encode_response(sink)?; info!("Sending '{:?}'", response); @@ -476,7 +476,8 @@ impl<'a> SftpHandler<'a> { } SftpPacket::Write(req_id, write) => { match self.file_server.write( - &write.handle, + &ObscuredFileHandle::from_filehandle(&write.handle) + .ok_or(WireError::BadString)?, write.offset, write.data.as_ref(), ) { @@ -488,7 +489,10 @@ impl<'a> SftpHandler<'a> { }; } SftpPacket::Close(req_id, close) => { - match self.file_server.close(&close.handle) { + match self.file_server.close( + &ObscuredFileHandle::from_filehandle(&close.handle) + .ok_or(WireError::BadString)?, + ) { Ok(_) => push_ok(req_id, sink)?, Err(e) => { error!("SFTP Close thrown: {:?}", e); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 0acbfc85..2ac34efc 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,13 +1,10 @@ -use crate::proto::{Attrs, FileHandle, Name, StatusCode}; +use crate::{ + ObscuredFileHandle, + proto::{Attrs, Name, StatusCode}, +}; use core::marker::PhantomData; -// TODO: enforce it and do checks for the FileHandle so it properly obscure the file path and user. -// Hint: In stateful server this can be done with a hash function and a dictionary -/// Used during storage of file handle data for long SFTP Write requests -/// Must be observed by SftpServer handle implementations -pub const FILE_HANDLE_MAX_LEN: usize = 256; - pub type SftpOpResult = core::result::Result; /// All trait functions are optional in the SFTP protocol. @@ -15,14 +12,12 @@ pub type SftpOpResult = core::result::Result; /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. pub trait SftpServer<'a> { - // type Handle<'a>: Into> + TryFrom> + Debug + Copy; - /// Opens a file or directory for reading/writing fn open( - &mut self, + &'_ mut self, filename: &str, attrs: &Attrs, - ) -> SftpOpResult> { + ) -> SftpOpResult { log::error!( "SftpServer Open operation not defined: filename = {:?}, attrs = {:?}", filename, @@ -32,7 +27,7 @@ pub trait SftpServer<'a> { } /// Close either a file or directory handle - fn close(&mut self, handle: &FileHandle) -> SftpOpResult<()> { + fn close(&mut self, handle: &ObscuredFileHandle) -> SftpOpResult<()> { log::error!("SftpServer Close operation not defined: handle = {:?}", handle); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -40,13 +35,13 @@ pub trait SftpServer<'a> { fn read( &mut self, - handle: &FileHandle, + obscured_file_handle: &ObscuredFileHandle, offset: u64, reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", - handle, + obscured_file_handle, offset ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -54,32 +49,32 @@ pub trait SftpServer<'a> { fn write( &mut self, - handle: &FileHandle, + obscured_file_handle: &ObscuredFileHandle, offset: u64, buf: &[u8], ) -> SftpOpResult<()> { log::error!( "SftpServer Write operation: handle = {:?}, offset = {:?}, buf = {:?}", - handle, + obscured_file_handle, offset, String::from_utf8(buf.to_vec()) ); Ok(()) } - fn opendir(&mut self, dir: &str) -> SftpOpResult> { + fn opendir(&mut self, dir: &str) -> SftpOpResult { log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } fn readdir( &mut self, - handle: &FileHandle, + obscured_file_handle: &ObscuredFileHandle, reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", - handle + obscured_file_handle ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } From 2d0800df982b66b3650e9cefb60dba6236607428 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 17 Sep 2025 16:26:06 +1000 Subject: [PATCH 098/393] Minimal SFTP Server implementation of write 64kB write pass the test File sent and written are identical. That is a good start. Next I will find what is wrong passed 64kB --- demo/sftp/std/src/demosftpserver.rs | 53 +++++++++++++++---- demo/sftp/std/src/main.rs | 7 +-- .../sftp/std/testing/test_only_file_upload.sh | 3 +- sftp/src/proto.rs | 6 +-- sftp/src/sftphandle.rs | 50 +++-------------- sftp/src/sftpserver.rs | 10 ++-- 6 files changed, 67 insertions(+), 62 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 737c7766..da291a84 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -5,10 +5,12 @@ use sunset_sftp::{ #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; +use std::{fs::File, os::unix::fs::FileExt}; struct PrivateFileHandler { file_path: String, permissions: Option, + file: File, } impl PathFinder for PrivateFileHandler { @@ -18,18 +20,16 @@ impl PathFinder for PrivateFileHandler { } pub struct DemoSftpServer { - user_path: String, + base_path: String, handlers_manager: HandleManager, } impl DemoSftpServer { - pub fn new(user: String) -> Self { - DemoSftpServer { - user_path: format!("/{}/", user.clone()), - handlers_manager: HandleManager::new(), - } + pub fn new(base_path: String) -> Self { + DemoSftpServer { base_path, handlers_manager: HandleManager::new() } } } + impl SftpServer<'_> for DemoSftpServer { // Mocking an Open operation. Will not check for permissions fn open( @@ -44,11 +44,31 @@ impl SftpServer<'_> for DemoSftpServer { return Err(StatusCode::SSH_FX_PERMISSION_DENIED); } + let poxit_attr = attrs + .permissions + .as_ref() + .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED)?; + let can_write = poxit_attr & 0o222 > 0; + let can_read = poxit_attr & 0o444 > 0; + debug!( + "File open for read/write access: can_read={:?}, can_write={:?}", + can_read, can_write + ); + + let file = File::options() + .read(can_read) + .write(can_write) + .create(true) + .open(filename) + .map_err(|_| StatusCode::SSH_FX_FAILURE)?; + let fh = self.handlers_manager.create_handle(PrivateFileHandler { file_path: filename.into(), permissions: attrs.permissions, + file, }); - warn!( + + debug!( "Filename \"{:?}\" will have the obscured file handle: {:?}", filename, fh ); @@ -59,7 +79,7 @@ impl SftpServer<'_> for DemoSftpServer { fn realpath(&mut self, dir: &str) -> SftpOpResult> { debug!("finding path for: {:?}", dir); Ok(Name(vec![NameEntry { - filename: Filename::from(self.user_path.as_str()), + filename: Filename::from(self.base_path.as_str()), _longname: Filename::from(""), attrs: Attrs { size: None, @@ -84,6 +104,7 @@ impl SftpServer<'_> for DemoSftpServer { "SftpServer Close operation on {:?} was successful", handle.file_path ); + drop(handle.file); // Not really required but illustrative Ok(()) } else { error!( @@ -113,13 +134,27 @@ impl SftpServer<'_> for DemoSftpServer { return Err(StatusCode::SSH_FX_PERMISSION_DENIED); }; - log::debug!( + log::trace!( "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buf = {:?}", obscured_file_handle, private_file_handle.file_path, offset, String::from_utf8(buf.to_vec()) ); + let bytes_written = private_file_handle + .file + .write_at(buf, offset) + .map_err(|_| StatusCode::SSH_FX_FAILURE)?; + + log::debug!( + "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buffer length = {:?}, bytes written = {:?}", + obscured_file_handle, + private_file_handle.file_path, + offset, + buf.len(), + bytes_written + ); + Ok(()) } diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 8d98977c..54aadd00 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -148,10 +148,11 @@ impl DemoServer for StdDemo { match { let mut stdio = serv.stdio(ch).await?; - let mut file_server = DemoSftpServer::new("user".to_string()); + let mut file_server = DemoSftpServer::new( + "./demo/sftp/std/testing/out/".to_string(), + ); - let mut sftp_handler = - SftpHandler::new(&mut file_server, buffer_in.len()); + let mut sftp_handler = SftpHandler::new(&mut file_server); loop { let lr = stdio.read(&mut buffer_in).await?; trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); diff --git a/demo/sftp/std/testing/test_only_file_upload.sh b/demo/sftp/std/testing/test_only_file_upload.sh index 92d34e71..465dabbb 100755 --- a/demo/sftp/std/testing/test_only_file_upload.sh +++ b/demo/sftp/std/testing/test_only_file_upload.sh @@ -32,7 +32,7 @@ put ./64kB_random bye EOF - +diff ./64kB_random ./out/64kB_random if [ $? -eq 0 ]; then echo "PASS" else @@ -41,5 +41,6 @@ fi echo "Cleaning up local files..." rm -f ./*_random +rm -f ./out/*_random echo "Upload test completed." \ No newline at end of file diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 17d21a96..252f1b4b 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -15,7 +15,7 @@ pub const SFTP_MINIMUM_PACKET_LEN: usize = 9; /// SFTP packets have the packet type after a u32 length field pub const SFTP_FIELD_ID_INDEX: usize = 4; /// SFTP packets ID length is 1 byte -pub const SFTP_FIELD_ID_LEN: usize = 1; +// pub const SFTP_FIELD_ID_LEN: usize = 1; /// SFTP packets start with the length field pub const SFTP_FIELD_LEN_INDEX: usize = 0; /// SFTP packets length field us u32 @@ -24,13 +24,13 @@ pub const SFTP_FIELD_LEN_LENGTH: usize = 4; // SSH_FXP_WRITE SFTP Packet definition used to decode long packets that do not fit in one buffer /// SFTP SSH_FXP_WRITE Packet cannot be shorter than this (len:4+pnum:1+rid:4+hand:4+0+data:4+0 bytes = 17 bytes) [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) -pub const SFTP_MINIMUM_WRITE_PACKET_LEN: usize = 17; +// pub const SFTP_MINIMUM_WRITE_PACKET_LEN: usize = 17; /// SFTP SSH_FXP_WRITE Packet request id field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) pub const SFTP_WRITE_REQID_INDEX: usize = 5; /// SFTP SSH_FXP_WRITE Packet handle field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) -pub const SFTP_WRITE_HANDLE_INDEX: usize = 9; +// pub const SFTP_WRITE_HANDLE_INDEX: usize = 9; // TODO is utf8 enough, or does this need to be an opaque binstring? #[derive(Debug, SSHEncode, SSHDecode)] diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 2315c59a..e7cdba85 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -20,7 +20,6 @@ use std::usize; struct PartialWriteRequestTracker { req_id: ReqId, obscure_file_handle: ObscuredFileHandle, // TODO: Change the file handle in SftpServer functions signature so it has a sort fixed length handle. - data_len: u32, remain_data_len: u32, remain_data_offset: u64, } @@ -29,18 +28,15 @@ impl PartialWriteRequestTracker { pub fn new( req_id: ReqId, obscure_file_handle: ObscuredFileHandle, - data_len: u32, remain_data_len: u32, remain_data_offset: u64, ) -> WireResult { - let mut ret = PartialWriteRequestTracker { + Ok(PartialWriteRequestTracker { req_id, obscure_file_handle: obscure_file_handle, - data_len, remain_data_len, remain_data_offset, - }; - Ok(ret) + }) } pub fn get_file_handle(&self) -> ObscuredFileHandle { @@ -100,25 +96,6 @@ impl<'de> SftpSource<'de> { } } - /// Peaks the buffer for packet length. This does not advance the reading index - /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail - /// - /// **Warning**: will only work in well formed packets, in other case the result will contain garbage - fn peak_packet_len(&self) -> WireResult { - if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - Err(WireError::PacketWrong) - } else { - let mut raw_bytes = [0u8; 4]; - raw_bytes.copy_from_slice( - &self.buffer[SFTP_FIELD_LEN_INDEX - ..SFTP_FIELD_LEN_INDEX + SFTP_FIELD_LEN_LENGTH], - ); - - Ok(u32::from_be_bytes(raw_bytes)) - } - } - /// Assuming that the buffer contains a Write request packet initial bytes, Peaks the buffer for the handle length. This does not advance the reading index /// /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail @@ -164,7 +141,6 @@ impl<'de> SftpSource<'de> { req_id, ObscuredFileHandle::from_filehandle(&file_handle) .ok_or(WireError::BadString)?, - data_len, remain_data_len, remain_data_offset, )?; @@ -229,38 +205,28 @@ impl<'g> SSHSink for SftpSink<'g> { //#[derive(Debug, Clone)] pub struct SftpHandler<'a> { + /// The local SFTP File server implementing the basic SFTP requests defined by `SftpServer` file_server: &'a mut dyn SftpServer<'a>, - /// - buffer_in_len: usize, + /// Once the client and the server have verified the agreed SFTP version the session is initialized initialized: bool, - // /// Use to process SFTP packets that have been received partially and the remaining is expected in successive buffers - // long_packet: bool, + /// Use to process SFTP Write packets that have been received partially and the remaining is expected in successive buffers partial_write_request_tracker: Option, } impl<'a> SftpHandler<'a> { - pub fn new( - file_server: &'a mut impl SftpServer<'a>, - buffer_len: usize, // max_file_handlers: u32 - ) -> Self { - if buffer_len < 256 { - warn!( - "Buffer length too small, must be at least 256 bytes. You are in uncharted territory" - ) - } + pub fn new(file_server: &'a mut impl SftpServer<'a>) -> Self { SftpHandler { file_server, - buffer_in_len: buffer_len, - // long_packet: false, + initialized: false, partial_write_request_tracker: None, } } /// Decodes the buffer_in request, process the request delegating operations to an Struct implementing SftpServer, - /// serialises an answer in buffer_out and return the length usedd in buffer_out + /// serializes an answer in buffer_out and return the length used in buffer_out pub async fn process( &mut self, buffer_in: &[u8], diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 2ac34efc..37a51949 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -37,7 +37,7 @@ pub trait SftpServer<'a> { &mut self, obscured_file_handle: &ObscuredFileHandle, offset: u64, - reply: &mut ReadReply<'_, '_>, + _reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", @@ -70,7 +70,7 @@ pub trait SftpServer<'a> { fn readdir( &mut self, obscured_file_handle: &ObscuredFileHandle, - reply: &mut DirReply<'_, '_>, + _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", @@ -86,20 +86,22 @@ pub trait SftpServer<'a> { } } +// TODO: Define this pub struct ReadReply<'g, 'a> { chan: ChanOut<'g, 'a>, } impl<'g, 'a> ReadReply<'g, 'a> { - pub fn reply(self, data: &[u8]) {} + pub fn reply(self, _data: &[u8]) {} } +// TODO: Define this pub struct DirReply<'g, 'a> { chan: ChanOut<'g, 'a>, } impl<'g, 'a> DirReply<'g, 'a> { - pub fn reply(self, data: &[u8]) {} + pub fn reply(self, _data: &[u8]) {} } // TODO: Implement correct Channel Out From 980fb554312be9ee89d655b028323948b16c72b7 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 17 Sep 2025 19:51:00 +1000 Subject: [PATCH 099/393] Troubleshooting the 65kB limit: There are two write requests I was assuming that the client would wait for a `SSH_FXP_STATUS` Ok, but it does not. I need to handle a second request segment in the same SftpSource --- demo/sftp/std/testing/test_limit_write_req.sh | 33 ++++++++++ .../sftp/std/testing/test_only_file_upload.sh | 46 ------------- demo/sftp/std/testing/test_write_requests.sh | 40 ++++++++++++ sftp/src/sftperror.rs | 17 +++++ sftp/src/sftphandle.rs | 64 ++++++++++++++----- 5 files changed, 137 insertions(+), 63 deletions(-) create mode 100755 demo/sftp/std/testing/test_limit_write_req.sh delete mode 100755 demo/sftp/std/testing/test_only_file_upload.sh create mode 100755 demo/sftp/std/testing/test_write_requests.sh create mode 100644 sftp/src/sftperror.rs diff --git a/demo/sftp/std/testing/test_limit_write_req.sh b/demo/sftp/std/testing/test_limit_write_req.sh new file mode 100755 index 00000000..45614183 --- /dev/null +++ b/demo/sftp/std/testing/test_limit_write_req.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Generate random data files +echo "Generating random data files..." + +dd if=/dev/random bs=1024 count=65 of=./TwoWriteRequests_random 2>/dev/null # Fails + + +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." +echo "Test Results:" +echo "=============" + +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +put ./TwoWriteRequests_random +bye +EOF + +diff ./TwoWriteRequests_random ./out/TwoWriteRequests_random +if [ $? -eq 0 ]; then + echo "PASS" +else + echo "FAIL" +fi + +echo "Cleaning up local files..." +rm -f ./*_random +rm -f ./out/*_random + +echo "Upload test completed." \ No newline at end of file diff --git a/demo/sftp/std/testing/test_only_file_upload.sh b/demo/sftp/std/testing/test_only_file_upload.sh deleted file mode 100755 index 465dabbb..00000000 --- a/demo/sftp/std/testing/test_only_file_upload.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -# Set remote server details -REMOTE_HOST="192.168.69.2" -REMOTE_USER="any" - -# Generate random data files -echo "Generating random data files..." -dd if=/dev/random bs=128 count=1 of=./128B_random 2>/dev/null -dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null -dd if=/dev/random bs=1024 count=2 of=./2kB_random 2>/dev/null -dd if=/dev/random bs=1024 count=4 of=./4kB_random 2>/dev/null -dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null -dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null # OK -dd if=/dev/random bs=1024 count=65 of=./MaxMaybe_random 2>/dev/null # Fails -dd if=/dev/random bs=1024 count=1024 of=./1MB_random 2>/dev/null - - -echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." -echo "Test Results:" -echo "=============" - -# put ./128B_random -# put ./512B_random -# put ./2kB_random -# put ./4kB_random -# put ./16kB_random -# put ./1MB_random -# put ./MaxMaybe_random -sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF -put ./64kB_random -bye -EOF - -diff ./64kB_random ./out/64kB_random -if [ $? -eq 0 ]; then - echo "PASS" -else - echo "FAIL" -fi - -echo "Cleaning up local files..." -rm -f ./*_random -rm -f ./out/*_random - -echo "Upload test completed." \ No newline at end of file diff --git a/demo/sftp/std/testing/test_write_requests.sh b/demo/sftp/std/testing/test_write_requests.sh new file mode 100755 index 00000000..dc615c1a --- /dev/null +++ b/demo/sftp/std/testing/test_write_requests.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Define test files +FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random") + +# Generate random data files +echo "Generating random data files..." +dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null +dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=65 of=./65kB_random 2>/dev/null + +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." + +# Upload all files +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +$(printf 'put ./%s\n' "${FILES[@]}") +bye +EOF + +echo "Test Results:" +echo "=============" + +# Test each file +for file in "${FILES[@]}"; do + if diff "./${file}" "./out/${file}" >/dev/null 2>&1; then + echo "PASS: ${file}" + else + echo "FAIL: ${file}" + fi +done + +echo "Cleaning up local files..." +rm -f ./*_random ./out/*_random + +echo "Upload test completed." \ No newline at end of file diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs new file mode 100644 index 00000000..b78393bb --- /dev/null +++ b/sftp/src/sftperror.rs @@ -0,0 +1,17 @@ +use core::convert::From; + +use sunset::sshwire::WireError; + +#[derive(Debug)] +pub enum SftpError { + WireError(WireError), + // SshError(SshError), +} + +impl From for SftpError { + fn from(value: WireError) -> Self { + SftpError::WireError(value) + } +} + +pub type SftpResult = Result; diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index e7cdba85..08cc031a 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,7 +1,7 @@ use crate::proto::{ - self, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, - SFTP_FIELD_LEN_LENGTH, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, - SFTP_WRITE_REQID_INDEX, SftpNum, SftpPacket, Status, StatusCode, + self, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_LENGTH, + SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SFTP_WRITE_REQID_INDEX, SftpNum, + SftpPacket, Status, StatusCode, }; use crate::sftpserver::SftpServer; use crate::{FileHandle, ObscuredFileHandle}; @@ -96,6 +96,24 @@ impl<'de> SftpSource<'de> { } } + /// Peaks the buffer for packet type adding an offset. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: This might only work in special conditions, such as those where the , in other case the result will contain garbage + fn peak_packet_type_with_offset( + &self, + starting_offset: usize, + ) -> WireResult { + // const SFTP_ID_BUFFER_INDEX: usize = 4; // All SFTP packet have the packet type after a u32 length field + // const SFTP_MINIMUM_LENGTH: usize = 9; // Corresponds to a minimal SSH_FXP_INIT packet + if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + Err(WireError::PacketWrong) + } else { + Ok(SftpNum::from(self.buffer[starting_offset + SFTP_FIELD_ID_INDEX])) + } + } + /// Assuming that the buffer contains a Write request packet initial bytes, Peaks the buffer for the handle length. This does not advance the reading index /// /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail @@ -149,12 +167,19 @@ impl<'de> SftpSource<'de> { } } - /// Used to decode the whole SSHSource as a single BinString + /// Used to decode the whole SSHSource as a single BinString ignoring the len field /// /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. fn dec_all_as_binstring(&mut self) -> WireResult> { Ok(BinString(self.take(self.buffer.len())?)) } + + /// Used to decode a slice of SSHSource as a single BinString ignoring the len field + /// + /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. + fn dec_as_binstring(&mut self, len: usize) -> WireResult> { + Ok(BinString(self.take(len)?)) + } } #[derive(Default)] @@ -242,39 +267,44 @@ impl<'a> SftpHandler<'a> { if let Some(mut write_tracker) = self.partial_write_request_tracker.take() { trace!( - "Processing successive chunks of a long write packet . Stored data: {:?}", + "Processing successive chunks of a long write packet. Stored data: {:?}", write_tracker ); + + let usable_data = in_len.min(write_tracker.remain_data_len as usize); + if in_len > write_tracker.remain_data_len as usize { // TODO: Investigate if we are receiving one packet and the beginning of the next one + let sftp_num = source.peak_packet_type_with_offset( + write_tracker.remain_data_len as usize, + )?; error!( - "There is too much data in the buffer! {:?} > than max expected {:?}", - in_len, write_tracker.remain_data_len + "There is too much data in the buffer! {:?} > than expected {:?}. There is a trailing packet in buffer: {:?}", + in_len, write_tracker.remain_data_len, sftp_num ); - return Err(WireError::PacketWrong); // TODO: Handle this error instead of failing. - } + }; - let current_write_offset = write_tracker.remain_data_offset; - let data_in_buffer = source.dec_all_as_binstring()?; + let data_segment = source.dec_as_binstring(usable_data)?; // TODO: Do proper casting with checks u32::try_from(data_in_buffer.0.len()) - let data_in_buffer_len = data_in_buffer.0.len() as u32; + let data_segment_len = data_segment.0.len() as u32; - write_tracker.remain_data_offset += data_in_buffer_len as u64; - write_tracker.remain_data_len -= data_in_buffer_len; + let current_write_offset = write_tracker.remain_data_offset; + write_tracker.remain_data_offset += data_segment_len as u64; + write_tracker.remain_data_len -= data_segment_len; let obscure_file_handle = write_tracker.get_file_handle(); debug!( - "Processing successive chunks of a long write packet. Writing : obscure_file_handle = {:?}, write_offset = {:?}, data = {:?}, data remaining = {:?}", + "Processing successive chunks of a long write packet. Writing : obscure_file_handle = {:?}, write_offset = {:?}, data_segment = {:?}, data remaining = {:?}", obscure_file_handle, current_write_offset, - data_in_buffer, + data_segment, write_tracker.remain_data_len ); match self.file_server.write( &obscure_file_handle, current_write_offset, - data_in_buffer.as_ref(), + data_segment.as_ref(), ) { Ok(_) => { if write_tracker.remain_data_len > 0 { From 79994a6c01d616dc8b585407c7fcfe0d64d1e606 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 19 Sep 2025 15:21:21 +1000 Subject: [PATCH 100/393] WIP: Refactoring opaque_file_handle -> obscure_file_handle and adding traits Will not compile ATM First, I am still working on the details to make the use of traits to define the DemoSftpServer flexible. In this commit there is too much rigidity in the DemoSftpServer which does not allow my intended vision. Second, there is an unrelated refactoring here where SFTPSink and SftpSource have been extracted from sftphandle.rs to their own files for clarity --- demo/sftp/std/src/demofilehandlemanager.rs | 60 ++++++ demo/sftp/std/src/demoopaquefilehandle.rs | 21 ++ demo/sftp/std/src/demosftpserver.rs | 87 ++++---- demo/sftp/std/src/main.rs | 13 +- sftp/src/lib.rs | 6 +- sftp/src/obscured_file_handle.rs | 219 ------------------- sftp/src/opaquefilehandle.rs | 66 ++++++ sftp/src/proto.rs | 4 + sftp/src/sftphandle.rs | 238 +++------------------ sftp/src/sftpserver.rs | 29 ++- sftp/src/sftpsink.rs | 52 +++++ sftp/src/sftpsource.rs | 143 +++++++++++++ 12 files changed, 451 insertions(+), 487 deletions(-) create mode 100644 demo/sftp/std/src/demofilehandlemanager.rs create mode 100644 demo/sftp/std/src/demoopaquefilehandle.rs delete mode 100644 sftp/src/obscured_file_handle.rs create mode 100644 sftp/src/opaquefilehandle.rs create mode 100644 sftp/src/sftpsink.rs create mode 100644 sftp/src/sftpsource.rs diff --git a/demo/sftp/std/src/demofilehandlemanager.rs b/demo/sftp/std/src/demofilehandlemanager.rs new file mode 100644 index 00000000..e7bae95a --- /dev/null +++ b/demo/sftp/std/src/demofilehandlemanager.rs @@ -0,0 +1,60 @@ +use sunset_sftp::{ + OpaqueFileHandle, OpaqueFileHandleManager, PathFinder, StatusCode, +}; + +use std::collections::HashMap; // Not enforced. Only for std. For no_std environments other solutions can be used to store Key, Value + +pub struct DemoFileHandleManager +where + K: OpaqueFileHandle, + V: PathFinder, +{ + handle_map: HashMap, +} + +impl DemoFileHandleManager +where + K: OpaqueFileHandle, + V: PathFinder, +{ + pub fn new() -> Self { + Self { handle_map: HashMap::new() } + } +} + +impl OpaqueFileHandleManager for DemoFileHandleManager +where + K: OpaqueFileHandle, + V: PathFinder, +{ + type Error = StatusCode; + + fn insert(&mut self, private_handle: V, salt: &str) -> Result { + if self + .handle_map + .iter() + .any(|(_, private_handle)| private_handle.matches(&private_handle)) + { + return Err(StatusCode::SSH_FX_PERMISSION_DENIED); + } + + let handle = K::new( + format!("{:}-{:}", &private_handle.get_path_ref(), salt).as_str(), + ); + + self.handle_map.insert(handle.clone(), private_handle); + Ok(handle) + } + + fn remove(&mut self, opaque_handle: &K) -> Option { + self.handle_map.remove(opaque_handle) + } + + fn opaque_handle_exist(&self, opaque_handle: &K) -> bool { + self.handle_map.contains_key(opaque_handle) + } + + fn get_private_as_ref(&self, opaque_handle: &K) -> Option<&V> { + self.handle_map.get(opaque_handle) + } +} diff --git a/demo/sftp/std/src/demoopaquefilehandle.rs b/demo/sftp/std/src/demoopaquefilehandle.rs new file mode 100644 index 00000000..44ecc2fe --- /dev/null +++ b/demo/sftp/std/src/demoopaquefilehandle.rs @@ -0,0 +1,21 @@ +use sunset_sftp::{FileHandle, OpaqueFileHandle}; + +#[derive(Debug, Hash, PartialEq, Eq, Clone)] +pub struct DemoOpaqueFileHandle {} + +impl OpaqueFileHandle for DemoOpaqueFileHandle { + fn new(seed: &str) -> Self { + todo!("Add some logic to create a hash form the &str from {:}", seed) + } + + fn try_from(file_handle: &FileHandle<'_>) -> sunset::sshwire::WireResult { + todo!( + "Add some logic to handle the the conversion try_from {:?}", + file_handle + ) + } + + fn into_file_handle(&self) -> FileHandle<'_> { + todo!("Add some logic to handle the the conversion into_file_handle") + } +} diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index da291a84..52c96aaf 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -1,49 +1,60 @@ +use crate::demofilehandlemanager::DemoFileHandleManager; + use sunset_sftp::{ - Attrs, DirReply, Filename, HandleManager, Name, NameEntry, ObscuredFileHandle, - PathFinder, ReadReply, SftpOpResult, SftpServer, StatusCode, + Attrs, DirReply, Filename, Name, NameEntry, OpaqueFileHandle, + OpaqueFileHandleManager, PathFinder, ReadReply, SftpOpResult, SftpServer, + StatusCode, }; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; use std::{fs::File, os::unix::fs::FileExt}; -struct PrivateFileHandler { +pub(crate) struct PrivateFileHandler { file_path: String, permissions: Option, file: File, } +static OPAQUE_SALT: &'static str = "12d%32"; + impl PathFinder for PrivateFileHandler { - fn matches_path(&self, path: &str) -> bool { - self.file_path.as_str().eq_ignore_ascii_case(path) + fn matches(&self, path: &PrivateFileHandler) -> bool { + self.file_path.as_str().eq_ignore_ascii_case(path.get_path_ref()) + } + + fn get_path_ref(&self) -> &str { + self.file_path.as_str() } } -pub struct DemoSftpServer { +pub struct DemoSftpServer +where + K: OpaqueFileHandle, + V: PathFinder, +{ base_path: String, - handlers_manager: HandleManager, + handlers_manager: DemoFileHandleManager, } -impl DemoSftpServer { +impl DemoSftpServer +where + K: OpaqueFileHandle, + V: PathFinder, +{ pub fn new(base_path: String) -> Self { - DemoSftpServer { base_path, handlers_manager: HandleManager::new() } + DemoSftpServer { base_path, handlers_manager: DemoFileHandleManager::new() } } } -impl SftpServer<'_> for DemoSftpServer { - // Mocking an Open operation. Will not check for permissions - fn open( - &mut self, - filename: &str, - attrs: &Attrs, - ) -> SftpOpResult { +impl SftpServer<'_, K> for DemoSftpServer +where + K: OpaqueFileHandle, + V: PathFinder, +{ + fn open(&mut self, filename: &str, attrs: &Attrs) -> SftpOpResult { debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); - if self.handlers_manager.is_open(filename) { - warn!("File {:?} already open, won't allow it", filename); - return Err(StatusCode::SSH_FX_PERMISSION_DENIED); - } - let poxit_attr = attrs .permissions .as_ref() @@ -62,18 +73,21 @@ impl SftpServer<'_> for DemoSftpServer { .open(filename) .map_err(|_| StatusCode::SSH_FX_FAILURE)?; - let fh = self.handlers_manager.create_handle(PrivateFileHandler { - file_path: filename.into(), - permissions: attrs.permissions, - file, - }); + let fh = self.handlers_manager.insert( + PrivateFileHandler { + file_path: filename.into(), + permissions: attrs.permissions, + file, + }, + OPAQUE_SALT, + ); debug!( "Filename \"{:?}\" will have the obscured file handle: {:?}", filename, fh ); - Ok(fh) + fh } fn realpath(&mut self, dir: &str) -> SftpOpResult> { @@ -93,13 +107,8 @@ impl SftpServer<'_> for DemoSftpServer { }])) } - fn close( - &mut self, - obscure_file_handle: &ObscuredFileHandle, - ) -> SftpOpResult<()> { - if let Some(handle) = - self.handlers_manager.remove_handle(obscure_file_handle) - { + fn close(&mut self, obscure_file_handle: &K) -> SftpOpResult<()> { + if let Some(handle) = self.handlers_manager.remove(obscure_file_handle) { debug!( "SftpServer Close operation on {:?} was successful", handle.file_path @@ -117,13 +126,13 @@ impl SftpServer<'_> for DemoSftpServer { fn write( &mut self, - obscured_file_handle: &ObscuredFileHandle, + obscured_file_handle: &K, offset: u64, buf: &[u8], ) -> SftpOpResult<()> { let private_file_handle = self .handlers_manager - .get_handle_value_as_ref(obscured_file_handle) + .get_private_as_ref(obscured_file_handle) .ok_or(StatusCode::SSH_FX_FAILURE)?; let permissions_poxit = (private_file_handle @@ -160,7 +169,7 @@ impl SftpServer<'_> for DemoSftpServer { fn read( &mut self, - obscured_file_handle: &ObscuredFileHandle, + obscured_file_handle: &K, offset: u64, _reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { @@ -172,14 +181,14 @@ impl SftpServer<'_> for DemoSftpServer { Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - fn opendir(&mut self, dir: &str) -> SftpOpResult { + fn opendir(&mut self, dir: &str) -> SftpOpResult { log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } fn readdir( &mut self, - obscured_file_handle: &ObscuredFileHandle, + obscured_file_handle: &K, _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 54aadd00..4a9799c7 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -6,7 +6,10 @@ pub(crate) use sunset_demo_common as demo_common; use demo_common::{DemoCommon, DemoServer, SSHConfig}; -use crate::demosftpserver::DemoSftpServer; +use crate::{ + demoopaquefilehandle::DemoOpaqueFileHandle, + demosftpserver::{DemoSftpServer, PrivateFileHandler}, +}; use embedded_io_async::{Read, Write}; @@ -23,6 +26,8 @@ use embassy_sync::channel::Channel; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; +mod demofilehandlemanager; +mod demoopaquefilehandle; mod demosftpserver; const NUM_LISTENERS: usize = 4; @@ -152,7 +157,11 @@ impl DemoServer for StdDemo { "./demo/sftp/std/testing/out/".to_string(), ); - let mut sftp_handler = SftpHandler::new(&mut file_server); + let mut sftp_handler = + SftpHandler::< + DemoOpaqueFileHandle, + DemoSftpServer, + >::new(&mut file_server); loop { let lr = stdio.read(&mut buffer_in).await?; trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 16a8ba76..0b9278a1 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,7 +1,9 @@ -mod obscured_file_handle; +mod opaquefilehandle; mod proto; mod sftphandle; mod sftpserver; +mod sftpsink; +mod sftpsource; pub use sftpserver::DirReply; pub use sftpserver::ReadReply; @@ -10,7 +12,7 @@ pub use sftpserver::SftpServer; pub use sftphandle::SftpHandler; -pub use obscured_file_handle::{HandleManager, ObscuredFileHandle, PathFinder}; +pub use opaquefilehandle::{OpaqueFileHandle, OpaqueFileHandleManager, PathFinder}; pub use proto::Attrs; pub use proto::FileHandle; diff --git a/sftp/src/obscured_file_handle.rs b/sftp/src/obscured_file_handle.rs deleted file mode 100644 index 34a678d4..00000000 --- a/sftp/src/obscured_file_handle.rs +++ /dev/null @@ -1,219 +0,0 @@ -use crate::FileHandle; - -use sunset::sshwire::{BinString, WireError}; - -use std::collections::HashMap; - -pub const FILE_HANDLE_MAX_LEN: usize = 8; - -/// Obscured file handle using Linear Congruential Generator (LCG) for pseudo-random generation. -/// -/// This struct provides a fixed-length handle that appears random but is deterministic -/// based on the input seed. Uses LCG with constants from Numerical Recipes. -/// -/// # Limitations -/// -/// - **Not cryptographically secure**: Predictable if the algorithm and seed are known -/// - **Limited entropy**: u8 seed provides only 256 different possible handles -/// - **Linear correlations**: Sequential seeds produce statistically correlated outputs -/// - **Reversible**: Given the handle, the original seed can potentially be recovered -/// - **Not suitable for security**: Should only be used for obscuration, not protection -/// -/// # Security Note -/// -/// This is intended for basic handle obscuration in SFTP to prevent casual observation -/// of handle-to-file mappings. It is NOT suitable for cryptographic purposes. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ObscuredFileHandle { - data: [u8; FILE_HANDLE_MAX_LEN], -} - -impl ObscuredFileHandle { - /// Generate a pseudo-random handle from a u8 seed using Linear Congruential Generator. - /// - /// Same seed will always produce the same handle. Only 256 different handles - /// are possible due to u8 seed limitation. This is deliberate to imply its limitations - /// - /// TODO: If this library is to be hardened this is a point to address - pub fn new(seed: u8) -> Self { - let mut data = [0u8; FILE_HANDLE_MAX_LEN]; - - // Simple Linear Congruential Generator (LCG) - // Using constants from Numerical Recipes - let mut state = seed as u64; - - for chunk in data.chunks_mut(8) { - // LCG: next = (a * current + c) mod 2^64 - state = state.wrapping_mul(1664525).wrapping_add(1013904223); - - let bytes = state.to_le_bytes(); - let copy_len = chunk.len().min(8); - chunk[..copy_len].copy_from_slice(&bytes[..copy_len]); - } - - Self { data } - } - - /// Get the handle as a byte slice - pub fn as_bytes(&self) -> &[u8] { - &self.data - } - - /// Get the handle as a Vec for wire protocol - pub fn to_vec(&self) -> Vec { - self.data.to_vec() - } - - /// Create from existing bytes (for parsing from wire) - pub fn from_bytes(bytes: &[u8]) -> Option { - if bytes.len() != FILE_HANDLE_MAX_LEN { - return None; - } - - let mut data = [0u8; FILE_HANDLE_MAX_LEN]; - data.copy_from_slice(bytes); - Some(Self { data }) - } - - /// Create from BinString (for parsing from SFTP wire protocol) - pub fn from_binstring(binstring: &BinString<'_>) -> Option { - Self::from_bytes(binstring.0) - } - - /// Create from FileHandle (for parsing from SFTP wire protocol) - pub fn from_filehandle(file_handle: &FileHandle<'_>) -> Option { - Self::from_bytes(file_handle.0.0) - } - - /// Convert to BinString for SFTP wire protocol - pub fn to_binstring(&self) -> BinString<'_> { - BinString(&self.data) - } - - /// Convert to FileHandle for SFTP wire protocol - pub fn to_filehandle(&self) -> FileHandle<'_> { - FileHandle(self.to_binstring()) - } -} - -// Display trait for debugging/logging -impl core::fmt::Display for ObscuredFileHandle { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - for byte in &self.data { - write!(f, "{:02x}", byte)?; - } - Ok(()) - } -} - -// Standard trait implementations for BinString conversion -impl<'a> From<&'a ObscuredFileHandle> for BinString<'a> { - fn from(handle: &'a ObscuredFileHandle) -> Self { - handle.to_binstring() - } -} - -impl<'a> TryFrom<&BinString<'a>> for ObscuredFileHandle { - type Error = WireError; - - fn try_from(binstring: &BinString<'a>) -> Result { - Self::from_binstring(binstring).ok_or(WireError::BadString) - } -} - -impl<'a> TryFrom> for ObscuredFileHandle { - type Error = WireError; - - fn try_from(binstring: BinString<'a>) -> Result { - Self::try_from(&binstring) - } -} - -// Conversions with proto::FileHandle -impl<'a> From<&'a ObscuredFileHandle> for crate::proto::FileHandle<'a> { - fn from(handle: &'a ObscuredFileHandle) -> Self { - crate::proto::FileHandle(handle.into()) - } -} - -impl<'a> TryFrom<&crate::proto::FileHandle<'a>> for ObscuredFileHandle { - type Error = WireError; - - fn try_from( - file_handle: &crate::proto::FileHandle<'a>, - ) -> Result { - Self::try_from(&file_handle.0) - } -} - -impl<'a> TryFrom> for ObscuredFileHandle { - type Error = WireError; - - fn try_from( - file_handle: crate::proto::FileHandle<'a>, - ) -> Result { - Self::try_from(&file_handle) - } -} - -/// Used to standarise finding a path within the HandleManager -pub trait PathFinder { - /// Helper function to find elements stored in the HandleManager that matches the give path - fn matches_path(&self, path: &str) -> bool; -} - -// Example usage structure for managing handles -pub struct HandleManager -where - T: PathFinder, -{ - next_handle_id: u8, - handle_map: HashMap, -} - -impl HandleManager { - pub fn new() -> Self { - Self { next_handle_id: 1, handle_map: HashMap::new() } - } - - pub fn create_handle(&mut self, value: T) -> ObscuredFileHandle { - let handle = ObscuredFileHandle::new(self.next_handle_id); - self.next_handle_id += 1; - - self.handle_map.insert(handle, value); - handle - } - - pub fn get_handle_value_as_ref( - &self, - handle: &ObscuredFileHandle, - ) -> Option<&T> { - self.handle_map.get(handle) - } - - pub fn remove_handle(&mut self, handle: &ObscuredFileHandle) -> Option { - self.handle_map.remove(handle) - } - - pub fn handle_exists(&self, handle: &ObscuredFileHandle) -> bool { - self.handle_map.contains_key(handle) - } - - pub fn is_open(&self, filename: &str) -> bool { - if self.handle_map.is_empty() { - return false; - } - self.handle_map.iter().any(|(_, element)| element.matches_path(filename)) - // TODO: Fix this. We need to be able to find out if the filename has been open. That cannot be done with a general T. Is will need to implement a trait to check that - // true - } -} - -impl Default for HandleManager -where - T: PathFinder, -{ - fn default() -> Self { - Self::new() - } -} diff --git a/sftp/src/opaquefilehandle.rs b/sftp/src/opaquefilehandle.rs new file mode 100644 index 00000000..2bdf739b --- /dev/null +++ b/sftp/src/opaquefilehandle.rs @@ -0,0 +1,66 @@ +use crate::FileHandle; + +use sunset::sshwire::WireResult; + +/// This is the trait with the required methods for interoperability between different opaque file handles +pub trait OpaqueFileHandle: + Sized + Clone + core::hash::Hash + PartialEq + Eq + core::fmt::Debug +{ + /// Creates a new instance using a given string slice as `seed` which + /// content should not clearly related to the seed + fn new(seed: &str) -> Self; + + /// Creates a new `OpaqueFileHandleTrait` copying the content of the `FileHandle` + fn try_from(file_handle: &FileHandle<'_>) -> WireResult; + + /// Returns a FileHandle pointing to the data in the `OpaqueFileHandleTrait` Implementation + fn into_file_handle(&self) -> FileHandle<'_>; +} + +/// Used to standardize finding a path within the HandleManager +/// +/// Must be implemented by the private handle structure to allow the `OpaqueHandleManager` to look for the path of the file itself +pub trait PathFinder { + /// Helper function to find elements stored in the HandleManager that matches the give path + fn matches(&self, path: &Self) -> bool; + + /// gets the path as a reference + fn get_path_ref(&self) -> &str; +} + +/// This trait is used to manage the OpaqueFile +/// +/// The SFTP module user is not required to use it but instead is a suggestion for an exchangeable +/// trait that facilitates structuring the store and retrieve of 'OpaqueFileHandleTrait' (K), +/// together with a private handle type or structure (V) that will contains all the details internally stored for the given file. +/// +/// The only requisite for v is that implements PathFinder, which in fact is another suggested helper to allow the `OpaqueHandleManager` +/// to look for the file path. +pub trait OpaqueFileHandleManager +where + K: OpaqueFileHandle, + V: PathFinder, +{ + /// The error used for all the trait members returning an error + type Error; + + // Excluded since it is too restrictive + // /// Performs any HandleManager Initialization + // fn new() -> Self; + + /// Given the private_handle, stores it and return an opaque file handle + /// + /// Returns an error if the private_handle has a matching path as obtained from `PathFinder` + /// + /// Salt has been added to allow the user to add a factor that will mask how the opaque handle is generated + fn insert(&mut self, private_handle: V, salt: &str) -> Result; + + /// + fn remove(&mut self, opaque_handle: &K) -> Option; + + /// Returns true if the opaque handle exist + fn opaque_handle_exist(&self, opaque_handle: &K) -> bool; + + /// given the opaque_handle returns a reference to the associated private handle + fn get_private_as_ref(&self, opaque_handle: &K) -> Option<&V>; +} diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 252f1b4b..01abe820 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -10,15 +10,19 @@ use num_enum::FromPrimitive; use paste::paste; /// SFTP Minimum packet length is 9 bytes corresponding with `SSH_FXP_INIT` +#[allow(unused)] pub const SFTP_MINIMUM_PACKET_LEN: usize = 9; /// SFTP packets have the packet type after a u32 length field +#[allow(unused)] pub const SFTP_FIELD_ID_INDEX: usize = 4; /// SFTP packets ID length is 1 byte // pub const SFTP_FIELD_ID_LEN: usize = 1; /// SFTP packets start with the length field +#[allow(unused)] pub const SFTP_FIELD_LEN_INDEX: usize = 0; /// SFTP packets length field us u32 +#[allow(unused)] pub const SFTP_FIELD_LEN_LENGTH: usize = 4; // SSH_FXP_WRITE SFTP Packet definition used to decode long packets that do not fit in one buffer diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 08cc031a..0d2357c0 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,14 +1,13 @@ +use crate::OpaqueFileHandle; use crate::proto::{ - self, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_LENGTH, - SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SFTP_WRITE_REQID_INDEX, SftpNum, + self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, SftpPacket, Status, StatusCode, }; use crate::sftpserver::SftpServer; -use crate::{FileHandle, ObscuredFileHandle}; +use crate::sftpsink::SftpSink; +use crate::sftpsource::SftpSource; -use sunset::sshwire::{ - BinString, SSHDecode, SSHSink, SSHSource, WireError, WireResult, -}; +use sunset::sshwire::{SSHDecode, WireError, WireResult}; use core::u32; #[allow(unused_imports)] @@ -17,17 +16,17 @@ use std::usize; /// Used to keep record of a long SFTP Write request that does not fit in receiving buffer and requires processing in batches #[derive(Debug)] -struct PartialWriteRequestTracker { +pub struct PartialWriteRequestTracker { req_id: ReqId, - obscure_file_handle: ObscuredFileHandle, // TODO: Change the file handle in SftpServer functions signature so it has a sort fixed length handle. + obscure_file_handle: T, remain_data_len: u32, remain_data_offset: u64, } -impl PartialWriteRequestTracker { +impl PartialWriteRequestTracker { pub fn new( req_id: ReqId, - obscure_file_handle: ObscuredFileHandle, + obscure_file_handle: T, remain_data_len: u32, remain_data_offset: u64, ) -> WireResult { @@ -39,212 +38,35 @@ impl PartialWriteRequestTracker { }) } - pub fn get_file_handle(&self) -> ObscuredFileHandle { + pub fn get_file_handle(&self) -> T { self.obscure_file_handle.clone() } } -/// SftpSource implements SSHSource and also extra functions to handle some challenges with long SFTP packets in constrained environments -#[derive(Default, Debug)] -pub struct SftpSource<'de> { - pub buffer: &'de [u8], - pub index: usize, -} - -impl<'de> SSHSource<'de> for SftpSource<'de> { - // Original take - fn take(&mut self, len: usize) -> sunset::sshwire::WireResult<&'de [u8]> { - if len + self.index > self.buffer.len() { - return Err(WireError::RanOut); - } - let original_index = self.index; - let slice = &self.buffer[self.index..self.index + len]; - self.index += len; - trace!( - "slice returned: {:?}. original index {:?}, new index: {:?}", - slice, original_index, self.index - ); - Ok(slice) - } - - fn remaining(&self) -> usize { - self.buffer.len() - self.index - } - - fn ctx(&mut self) -> &mut sunset::packets::ParseContext { - todo!("Which context for sftp?"); - } -} - -impl<'de> SftpSource<'de> { - pub fn new(buffer: &'de [u8]) -> Self { - SftpSource { buffer: buffer, index: 0 } - } - - /// Peaks the buffer for packet type. This does not advance the reading index - /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail - /// - /// **Warning**: will only work in well formed packets, in other case the result will contain garbage - fn peak_packet_type(&self) -> WireResult { - // const SFTP_ID_BUFFER_INDEX: usize = 4; // All SFTP packet have the packet type after a u32 length field - // const SFTP_MINIMUM_LENGTH: usize = 9; // Corresponds to a minimal SSH_FXP_INIT packet - if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - Err(WireError::PacketWrong) - } else { - Ok(SftpNum::from(self.buffer[SFTP_FIELD_ID_INDEX])) - } - } - - /// Peaks the buffer for packet type adding an offset. This does not advance the reading index - /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail - /// - /// **Warning**: This might only work in special conditions, such as those where the , in other case the result will contain garbage - fn peak_packet_type_with_offset( - &self, - starting_offset: usize, - ) -> WireResult { - // const SFTP_ID_BUFFER_INDEX: usize = 4; // All SFTP packet have the packet type after a u32 length field - // const SFTP_MINIMUM_LENGTH: usize = 9; // Corresponds to a minimal SSH_FXP_INIT packet - if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - Err(WireError::PacketWrong) - } else { - Ok(SftpNum::from(self.buffer[starting_offset + SFTP_FIELD_ID_INDEX])) - } - } - - /// Assuming that the buffer contains a Write request packet initial bytes, Peaks the buffer for the handle length. This does not advance the reading index - /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail - /// - /// **Warning**: will only work in well formed write packets, in other case the result will contain garbage - fn get_packet_partial_write_content_and_tracker( - &mut self, - ) -> WireResult<( - ObscuredFileHandle, - ReqId, - u64, - BinString<'de>, - PartialWriteRequestTracker, - )> { - if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - Err(WireError::PacketWrong) - } else { - let prev_index = self.index; - self.index = SFTP_WRITE_REQID_INDEX; - let req_id = ReqId::dec(self)?; - let file_handle = FileHandle::dec(self)?; - - let obscured_file_handle = - ObscuredFileHandle::from_binstring(&file_handle.0) - .ok_or(WireError::BadString)?; - - let offset = u64::dec(self)?; - let data_len = u32::dec(self)?; - - let data_len_in_buffer = self.buffer.len() - self.index; - let data_in_buffer = BinString(self.take(data_len_in_buffer)?); - - self.index = prev_index; - - let remain_data_len = data_len - data_len_in_buffer as u32; - let remain_data_offset = offset + data_len_in_buffer as u64; - trace!( - "Request ID = {:?}, Handle = {:?}, offset = {:?}, data length in buffer = {:?}, data in current buffer {:?} ", - req_id, file_handle, offset, data_len_in_buffer, data_in_buffer - ); - - let write_tracker = PartialWriteRequestTracker::new( - req_id, - ObscuredFileHandle::from_filehandle(&file_handle) - .ok_or(WireError::BadString)?, - remain_data_len, - remain_data_offset, - )?; - - Ok((obscured_file_handle, req_id, offset, data_in_buffer, write_tracker)) - } - } - - /// Used to decode the whole SSHSource as a single BinString ignoring the len field - /// - /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. - fn dec_all_as_binstring(&mut self) -> WireResult> { - Ok(BinString(self.take(self.buffer.len())?)) - } - - /// Used to decode a slice of SSHSource as a single BinString ignoring the len field - /// - /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. - fn dec_as_binstring(&mut self, len: usize) -> WireResult> { - Ok(BinString(self.take(len)?)) - } -} - -#[derive(Default)] -pub struct SftpSink<'g> { - pub buffer: &'g mut [u8], - index: usize, -} - -impl<'g> SftpSink<'g> { - pub fn new(s: &'g mut [u8]) -> Self { - SftpSink { buffer: s, index: SFTP_FIELD_LEN_LENGTH } - } - - /// Finalise the buffer by prepending the payload size and returning - /// - /// Returns the final index in the buffer as a reference for the space used - pub fn finalize(&mut self) -> usize { - if self.index <= SFTP_FIELD_LEN_LENGTH { - warn!("SftpSink trying to terminate it before pushing data"); - return 0; - } // size is 0 - let used_size = (self.index - SFTP_FIELD_LEN_LENGTH) as u32; - - used_size - .to_be_bytes() - .iter() - .enumerate() - .for_each(|(i, v)| self.buffer[i] = *v); - - self.index - } -} - -impl<'g> SSHSink for SftpSink<'g> { - fn push(&mut self, v: &[u8]) -> sunset::sshwire::WireResult<()> { - if v.len() + self.index > self.buffer.len() { - return Err(WireError::NoRoom); - } - trace!("Sink index: {:}", self.index); - v.iter().for_each(|val| { - self.buffer[self.index] = *val; - self.index += 1; - }); - trace!("Sink new index: {:}", self.index); - Ok(()) - } -} - //#[derive(Debug, Clone)] -pub struct SftpHandler<'a> { +pub struct SftpHandler<'a, T, S> +where + T: OpaqueFileHandle, + S: SftpServer<'a, T>, +{ /// The local SFTP File server implementing the basic SFTP requests defined by `SftpServer` - file_server: &'a mut dyn SftpServer<'a>, + file_server: &'a mut S, /// Once the client and the server have verified the agreed SFTP version the session is initialized initialized: bool, /// Use to process SFTP Write packets that have been received partially and the remaining is expected in successive buffers - partial_write_request_tracker: Option, + partial_write_request_tracker: Option>, } -impl<'a> SftpHandler<'a> { - pub fn new(file_server: &'a mut impl SftpServer<'a>) -> Self { +impl<'a, T, S> SftpHandler<'a, T, S> +where + T: OpaqueFileHandle, + S: SftpServer<'a, T>, +{ + pub fn new(file_server: &'a mut S) -> Self { SftpHandler { file_server, - initialized: false, partial_write_request_tracker: None, } @@ -274,7 +96,7 @@ impl<'a> SftpHandler<'a> { let usable_data = in_len.min(write_tracker.remain_data_len as usize); if in_len > write_tracker.remain_data_len as usize { - // TODO: Investigate if we are receiving one packet and the beginning of the next one + // This is because we are receiving one packet and the beginning of the next one let sftp_num = source.peak_packet_type_with_offset( write_tracker.remain_data_len as usize, )?; @@ -454,11 +276,11 @@ impl<'a> SftpHandler<'a> { } SftpPacket::Open(req_id, open) => { match self.file_server.open(open.filename.as_str()?, &open.attrs) { - Ok(obscured_file_handle) => { + Ok(opaque_file_handle) => { let response = SftpPacket::Handle( req_id, proto::Handle { - handle: obscured_file_handle.to_filehandle(), + handle: opaque_file_handle.into_file_handle(), }, ); response.encode_response(sink)?; @@ -472,8 +294,7 @@ impl<'a> SftpHandler<'a> { } SftpPacket::Write(req_id, write) => { match self.file_server.write( - &ObscuredFileHandle::from_filehandle(&write.handle) - .ok_or(WireError::BadString)?, + &T::try_from(&write.handle)?, write.offset, write.data.as_ref(), ) { @@ -485,10 +306,7 @@ impl<'a> SftpHandler<'a> { }; } SftpPacket::Close(req_id, close) => { - match self.file_server.close( - &ObscuredFileHandle::from_filehandle(&close.handle) - .ok_or(WireError::BadString)?, - ) { + match self.file_server.close(&T::try_from(&close.handle)?) { Ok(_) => push_ok(req_id, sink)?, Err(e) => { error!("SFTP Close thrown: {:?}", e); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 37a51949..201c7eb3 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,5 +1,5 @@ use crate::{ - ObscuredFileHandle, + OpaqueFileHandle, proto::{Attrs, Name, StatusCode}, }; @@ -11,13 +11,12 @@ pub type SftpOpResult = core::result::Result; /// Some less core operations have a Provided implementation returning /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. -pub trait SftpServer<'a> { +pub trait SftpServer<'a, T> +where + T: OpaqueFileHandle, +{ /// Opens a file or directory for reading/writing - fn open( - &'_ mut self, - filename: &str, - attrs: &Attrs, - ) -> SftpOpResult { + fn open(&'_ mut self, filename: &str, attrs: &Attrs) -> SftpOpResult { log::error!( "SftpServer Open operation not defined: filename = {:?}, attrs = {:?}", filename, @@ -27,7 +26,7 @@ pub trait SftpServer<'a> { } /// Close either a file or directory handle - fn close(&mut self, handle: &ObscuredFileHandle) -> SftpOpResult<()> { + fn close(&mut self, handle: &T) -> SftpOpResult<()> { log::error!("SftpServer Close operation not defined: handle = {:?}", handle); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -35,13 +34,13 @@ pub trait SftpServer<'a> { fn read( &mut self, - obscured_file_handle: &ObscuredFileHandle, + opaque_file_handle: &T, offset: u64, _reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", - obscured_file_handle, + opaque_file_handle, offset ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -49,32 +48,32 @@ pub trait SftpServer<'a> { fn write( &mut self, - obscured_file_handle: &ObscuredFileHandle, + opaque_file_handle: &T, offset: u64, buf: &[u8], ) -> SftpOpResult<()> { log::error!( "SftpServer Write operation: handle = {:?}, offset = {:?}, buf = {:?}", - obscured_file_handle, + opaque_file_handle, offset, String::from_utf8(buf.to_vec()) ); Ok(()) } - fn opendir(&mut self, dir: &str) -> SftpOpResult { + fn opendir(&mut self, dir: &str) -> SftpOpResult { log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } fn readdir( &mut self, - obscured_file_handle: &ObscuredFileHandle, + opaque_file_handle: &T, _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", - obscured_file_handle + opaque_file_handle ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } diff --git a/sftp/src/sftpsink.rs b/sftp/src/sftpsink.rs new file mode 100644 index 00000000..f3bb85c3 --- /dev/null +++ b/sftp/src/sftpsink.rs @@ -0,0 +1,52 @@ +use crate::proto::SFTP_FIELD_LEN_LENGTH; + +use sunset::sshwire::{SSHSink, WireError}; + +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; + +#[derive(Default)] +pub struct SftpSink<'g> { + pub buffer: &'g mut [u8], + index: usize, +} + +impl<'g> SftpSink<'g> { + pub fn new(s: &'g mut [u8]) -> Self { + SftpSink { buffer: s, index: SFTP_FIELD_LEN_LENGTH } + } + + /// Finalise the buffer by prepending the payload size and returning + /// + /// Returns the final index in the buffer as a reference for the space used + pub fn finalize(&mut self) -> usize { + if self.index <= SFTP_FIELD_LEN_LENGTH { + warn!("SftpSink trying to terminate it before pushing data"); + return 0; + } // size is 0 + let used_size = (self.index - SFTP_FIELD_LEN_LENGTH) as u32; + + used_size + .to_be_bytes() + .iter() + .enumerate() + .for_each(|(i, v)| self.buffer[i] = *v); + + self.index + } +} + +impl<'g> SSHSink for SftpSink<'g> { + fn push(&mut self, v: &[u8]) -> sunset::sshwire::WireResult<()> { + if v.len() + self.index > self.buffer.len() { + return Err(WireError::NoRoom); + } + trace!("Sink index: {:}", self.index); + v.iter().for_each(|val| { + self.buffer[self.index] = *val; + self.index += 1; + }); + trace!("Sink new index: {:}", self.index); + Ok(()) + } +} diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs new file mode 100644 index 00000000..9c97cc85 --- /dev/null +++ b/sftp/src/sftpsource.rs @@ -0,0 +1,143 @@ +use crate::proto::{ + ReqId, SFTP_FIELD_ID_INDEX, SFTP_MINIMUM_PACKET_LEN, SFTP_WRITE_REQID_INDEX, + SftpNum, +}; +use crate::sftphandle::PartialWriteRequestTracker; +use crate::{FileHandle, OpaqueFileHandle}; + +use sunset::sshwire::{BinString, SSHDecode, SSHSource, WireError, WireResult}; + +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; + +/// SftpSource implements SSHSource and also extra functions to handle some challenges with long SFTP packets in constrained environments +#[derive(Default, Debug)] +pub struct SftpSource<'de> { + pub buffer: &'de [u8], + pub index: usize, +} + +impl<'de> SSHSource<'de> for SftpSource<'de> { + // Original take + fn take(&mut self, len: usize) -> sunset::sshwire::WireResult<&'de [u8]> { + if len + self.index > self.buffer.len() { + return Err(WireError::RanOut); + } + let original_index = self.index; + let slice = &self.buffer[self.index..self.index + len]; + self.index += len; + trace!( + "slice returned: {:?}. original index {:?}, new index: {:?}", + slice, original_index, self.index + ); + Ok(slice) + } + + fn remaining(&self) -> usize { + self.buffer.len() - self.index + } + + fn ctx(&mut self) -> &mut sunset::packets::ParseContext { + todo!("Which context for sftp?"); + } +} + +impl<'de> SftpSource<'de> { + pub fn new(buffer: &'de [u8]) -> Self { + SftpSource { buffer: buffer, index: 0 } + } + + /// Peaks the buffer for packet type. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: will only work in well formed packets, in other case the result will contain garbage + pub(crate) fn peak_packet_type(&self) -> WireResult { + if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + Err(WireError::PacketWrong) + } else { + Ok(SftpNum::from(self.buffer[SFTP_FIELD_ID_INDEX])) + } + } + + /// Peaks the buffer for packet type adding an offset. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: This might only work in special conditions, such as those where the , in other case the result will contain garbage + pub(crate) fn peak_packet_type_with_offset( + &self, + starting_offset: usize, + ) -> WireResult { + // const SFTP_ID_BUFFER_INDEX: usize = 4; // All SFTP packet have the packet type after a u32 length field + // const SFTP_MINIMUM_LENGTH: usize = 9; // Corresponds to a minimal SSH_FXP_INIT packet + if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + Err(WireError::PacketWrong) + } else { + Ok(SftpNum::from(self.buffer[starting_offset + SFTP_FIELD_ID_INDEX])) + } + } + + /// Assuming that the buffer contains a Write request packet initial bytes, Peaks the buffer for the handle length. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: will only work in well formed write packets, in other case the result will contain garbage + pub(crate) fn get_packet_partial_write_content_and_tracker< + T: OpaqueFileHandle, + >( + &mut self, + ) -> WireResult<(T, ReqId, u64, BinString<'de>, PartialWriteRequestTracker)> + { + if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + Err(WireError::PacketWrong) + } else { + let prev_index = self.index; + self.index = SFTP_WRITE_REQID_INDEX; + let req_id = ReqId::dec(self)?; + let file_handle = FileHandle::dec(self)?; + + let obscured_file_handle = OpaqueFileHandle::try_from(&file_handle)?; + let offset = u64::dec(self)?; + let data_len = u32::dec(self)?; + + let data_len_in_buffer = self.buffer.len() - self.index; + let data_in_buffer = BinString(self.take(data_len_in_buffer)?); + + self.index = prev_index; + + let remain_data_len = data_len - data_len_in_buffer as u32; + let remain_data_offset = offset + data_len_in_buffer as u64; + trace!( + "Request ID = {:?}, Handle = {:?}, offset = {:?}, data length in buffer = {:?}, data in current buffer {:?} ", + req_id, file_handle, offset, data_len_in_buffer, data_in_buffer + ); + + let write_tracker = PartialWriteRequestTracker::new( + req_id, + OpaqueFileHandle::try_from(&file_handle)?, + remain_data_len, + remain_data_offset, + )?; + + Ok((obscured_file_handle, req_id, offset, data_in_buffer, write_tracker)) + } + } + + /// Used to decode the whole SSHSource as a single BinString ignoring the len field + /// + /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. + pub(crate) fn dec_all_as_binstring(&mut self) -> WireResult> { + Ok(BinString(self.take(self.buffer.len())?)) + } + + /// Used to decode a slice of SSHSource as a single BinString ignoring the len field + /// + /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. + pub(crate) fn dec_as_binstring( + &mut self, + len: usize, + ) -> WireResult> { + Ok(BinString(self.take(len)?)) + } +} From 5d9be17be3f205d1cf7c376f6b22d86d8258f2fa Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 19 Sep 2025 16:36:47 +1000 Subject: [PATCH 101/393] WIP: Fixed DemoSftpServer definition to use a custom implementation This allows the library user to define both a private and opaque file handle --- demo/sftp/std/src/demosftpserver.rs | 50 ++++++++++++++--------------- demo/sftp/std/src/main.rs | 7 ++-- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 52c96aaf..aea93817 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -1,9 +1,11 @@ -use crate::demofilehandlemanager::DemoFileHandleManager; +use crate::{ + demofilehandlemanager::DemoFileHandleManager, + demoopaquefilehandle::DemoOpaqueFileHandle, +}; use sunset_sftp::{ - Attrs, DirReply, Filename, Name, NameEntry, OpaqueFileHandle, - OpaqueFileHandleManager, PathFinder, ReadReply, SftpOpResult, SftpServer, - StatusCode, + Attrs, DirReply, Filename, Name, NameEntry, OpaqueFileHandleManager, PathFinder, + ReadReply, SftpOpResult, SftpServer, StatusCode, }; #[allow(unused_imports)] @@ -28,31 +30,24 @@ impl PathFinder for PrivateFileHandler { } } -pub struct DemoSftpServer -where - K: OpaqueFileHandle, - V: PathFinder, -{ +pub struct DemoSftpServer { base_path: String, - handlers_manager: DemoFileHandleManager, + handlers_manager: + DemoFileHandleManager, } -impl DemoSftpServer -where - K: OpaqueFileHandle, - V: PathFinder, -{ +impl DemoSftpServer { pub fn new(base_path: String) -> Self { DemoSftpServer { base_path, handlers_manager: DemoFileHandleManager::new() } } } -impl SftpServer<'_, K> for DemoSftpServer -where - K: OpaqueFileHandle, - V: PathFinder, -{ - fn open(&mut self, filename: &str, attrs: &Attrs) -> SftpOpResult { +impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { + fn open( + &mut self, + filename: &str, + attrs: &Attrs, + ) -> SftpOpResult { debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); let poxit_attr = attrs @@ -107,7 +102,10 @@ where }])) } - fn close(&mut self, obscure_file_handle: &K) -> SftpOpResult<()> { + fn close( + &mut self, + obscure_file_handle: &DemoOpaqueFileHandle, + ) -> SftpOpResult<()> { if let Some(handle) = self.handlers_manager.remove(obscure_file_handle) { debug!( "SftpServer Close operation on {:?} was successful", @@ -126,7 +124,7 @@ where fn write( &mut self, - obscured_file_handle: &K, + obscured_file_handle: &DemoOpaqueFileHandle, offset: u64, buf: &[u8], ) -> SftpOpResult<()> { @@ -169,7 +167,7 @@ where fn read( &mut self, - obscured_file_handle: &K, + obscured_file_handle: &DemoOpaqueFileHandle, offset: u64, _reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { @@ -181,14 +179,14 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - fn opendir(&mut self, dir: &str) -> SftpOpResult { + fn opendir(&mut self, dir: &str) -> SftpOpResult { log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } fn readdir( &mut self, - obscured_file_handle: &K, + obscured_file_handle: &DemoOpaqueFileHandle, _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 4a9799c7..0644aa67 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -158,10 +158,9 @@ impl DemoServer for StdDemo { ); let mut sftp_handler = - SftpHandler::< - DemoOpaqueFileHandle, - DemoSftpServer, - >::new(&mut file_server); + SftpHandler::::new( + &mut file_server, + ); loop { let lr = stdio.read(&mut buffer_in).await?; trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); From a19b0c6aa5ffa2ddb19296d97eed0d15519b3972 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 19 Sep 2025 17:10:20 +1000 Subject: [PATCH 102/393] WIP: Minimal implementation for DemoSftpServer Still a work in progress, but extract the definition of: - OpaqueFileHandle trait struct: to allow library users to define the OpaqueFileHandle at will - OpaqueFileHandle trait struct: A proposed structure trait to allow the users defining its own OpaqueFileHandle and InternalHandle Manager This increase the flexibility of the library allowing users taking decisions of their application rather than forcing them to use predefined elements Next step: Handle consecutive requests that might be received in the same buffer --- demo/sftp/std/Cargo.toml | 1 + demo/sftp/std/src/demoopaquefilehandle.rs | 29 +++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/demo/sftp/std/Cargo.toml b/demo/sftp/std/Cargo.toml index 0c2a8205..1cce9708 100644 --- a/demo/sftp/std/Cargo.toml +++ b/demo/sftp/std/Cargo.toml @@ -37,3 +37,4 @@ async-io = "1.6.0" critical-section = "1.1" rand = { version = "0.8", default-features = false, features = ["getrandom"] } sha2 = { version = "0.10", default-features = false } +fnv = "1.0.7" diff --git a/demo/sftp/std/src/demoopaquefilehandle.rs b/demo/sftp/std/src/demoopaquefilehandle.rs index 44ecc2fe..b7a1b92c 100644 --- a/demo/sftp/std/src/demoopaquefilehandle.rs +++ b/demo/sftp/std/src/demoopaquefilehandle.rs @@ -1,21 +1,36 @@ use sunset_sftp::{FileHandle, OpaqueFileHandle}; +use sunset::sshwire::{BinString, WireError}; + +use core::hash::Hasher; + +use fnv::FnvHasher; + +const HASH_LEN: usize = 4; #[derive(Debug, Hash, PartialEq, Eq, Clone)] -pub struct DemoOpaqueFileHandle {} +pub(crate) struct DemoOpaqueFileHandle { + tiny_hash: [u8; HASH_LEN], +} impl OpaqueFileHandle for DemoOpaqueFileHandle { fn new(seed: &str) -> Self { - todo!("Add some logic to create a hash form the &str from {:}", seed) + let mut hasher = FnvHasher::default(); + hasher.write(seed.as_bytes()); + DemoOpaqueFileHandle { tiny_hash: (hasher.finish() as u32).to_be_bytes() } } fn try_from(file_handle: &FileHandle<'_>) -> sunset::sshwire::WireResult { - todo!( - "Add some logic to handle the the conversion try_from {:?}", - file_handle - ) + if !file_handle.0 .0.len().eq(&core::mem::size_of::()) + { + return Err(WireError::BadString); + } + + let mut tiny_hash = [0u8; HASH_LEN]; + tiny_hash.copy_from_slice(file_handle.0 .0); + Ok(DemoOpaqueFileHandle { tiny_hash }) } fn into_file_handle(&self) -> FileHandle<'_> { - todo!("Add some logic to handle the the conversion into_file_handle") + FileHandle(BinString(&self.tiny_hash)) } } From f7524a9ed05aa91326b7032d6c10825dbbadc504 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 19 Sep 2025 17:23:51 +1000 Subject: [PATCH 103/393] opaque_file_handle not obscure_file_handle --- demo/sftp/std/src/demosftpserver.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index aea93817..79591175 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -104,9 +104,9 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { fn close( &mut self, - obscure_file_handle: &DemoOpaqueFileHandle, + opaque_file_handle: &DemoOpaqueFileHandle, ) -> SftpOpResult<()> { - if let Some(handle) = self.handlers_manager.remove(obscure_file_handle) { + if let Some(handle) = self.handlers_manager.remove(opaque_file_handle) { debug!( "SftpServer Close operation on {:?} was successful", handle.file_path @@ -116,7 +116,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } else { error!( "SftpServer Close operation on handle {:?} failed", - obscure_file_handle + opaque_file_handle ); Err(StatusCode::SSH_FX_FAILURE) } @@ -124,13 +124,13 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { fn write( &mut self, - obscured_file_handle: &DemoOpaqueFileHandle, + opaque_file_handle: &DemoOpaqueFileHandle, offset: u64, buf: &[u8], ) -> SftpOpResult<()> { let private_file_handle = self .handlers_manager - .get_private_as_ref(obscured_file_handle) + .get_private_as_ref(opaque_file_handle) .ok_or(StatusCode::SSH_FX_FAILURE)?; let permissions_poxit = (private_file_handle @@ -143,7 +143,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { log::trace!( "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buf = {:?}", - obscured_file_handle, + opaque_file_handle, private_file_handle.file_path, offset, String::from_utf8(buf.to_vec()) @@ -155,7 +155,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { log::debug!( "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buffer length = {:?}, bytes written = {:?}", - obscured_file_handle, + opaque_file_handle, private_file_handle.file_path, offset, buf.len(), @@ -167,13 +167,13 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { fn read( &mut self, - obscured_file_handle: &DemoOpaqueFileHandle, + opaque_file_handle: &DemoOpaqueFileHandle, offset: u64, _reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", - obscured_file_handle, + opaque_file_handle, offset ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -186,12 +186,12 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { fn readdir( &mut self, - obscured_file_handle: &DemoOpaqueFileHandle, + opaque_file_handle: &DemoOpaqueFileHandle, _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", - obscured_file_handle + opaque_file_handle ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } From cabec4e96c52d8bc5b09c44e917e4b770f8d407e Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 19 Sep 2025 18:07:38 +1000 Subject: [PATCH 104/393] Letting the `sftp_loop` and `prog_loop` finish DemoServer.run() This way the common server closes the socket and SFTP clients do not get hanged on failure or bye --- demo/sftp/std/src/main.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 0644aa67..63be11e3 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -181,9 +181,11 @@ impl DemoServer for StdDemo { } { Ok(_) => { warn!("sftp server loop finished gracefully"); + return Ok(()); } Err(e) => { - warn!("sftp server loop finished with an error: {}", e) + error!("sftp server loop finished with an error: {}", e); + return Err(e); } }; } @@ -191,8 +193,16 @@ impl DemoServer for StdDemo { }; let selected = select(prog_loop, sftp_loop).await; - error!("Selected finished: {:?}", selected); - todo!("Loop terminated: {:?}", selected) + match selected { + embassy_futures::select::Either::First(res) => { + warn!("prog_loop finished: {:?}", res); + res + } + embassy_futures::select::Either::Second(res) => { + warn!("sftp_loop finished: {:?}", res); + res + } + } } } From 4e900e68ad04bb56c594fe7a51987018215dd2bc Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 23 Sep 2025 20:05:09 +1000 Subject: [PATCH 105/393] Tiding up Added change to Cargo.lock is part of commit a19b0c6aa5ffa2ddb19296d97eed0d15519b3972 Removed cargo-example-sftp.rs, used for macro_rules troubleshooting not required at the moment --- Cargo.lock | 1 + sftp/out/cargo-expand-sftp.rs | 1800 --------------------------------- 2 files changed, 1 insertion(+), 1800 deletions(-) delete mode 100644 sftp/out/cargo-expand-sftp.rs diff --git a/Cargo.lock b/Cargo.lock index e709aa23..17e2beb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2863,6 +2863,7 @@ dependencies = [ "embassy-time", "embedded-io-async", "env_logger", + "fnv", "heapless", "libc", "log", diff --git a/sftp/out/cargo-expand-sftp.rs b/sftp/out/cargo-expand-sftp.rs deleted file mode 100644 index 0032029e..00000000 --- a/sftp/out/cargo-expand-sftp.rs +++ /dev/null @@ -1,1800 +0,0 @@ -#![feature(prelude_import)] -#[prelude_import] -use std::prelude::rust_2024::*; -#[macro_use] -extern crate std; -mod proto { - use num_enum::FromPrimitive; - use paste::paste; - use sunset::sshwire::{ - BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, - WireResult, - }; - use sunset_sshwire_derive::{SSHDecode, SSHEncode}; - #[allow(unused_imports)] - use log::{debug, error, info, log, trace, warn}; - pub struct Filename<'a>(TextString<'a>); - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Filename<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Filename", &&self.0) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Filename<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.0, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Filename<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) - } - } - pub struct FileHandle<'a>(pub BinString<'a>); - #[automatically_derived] - impl<'a> ::core::fmt::Debug for FileHandle<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_tuple_field1_finish(f, "FileHandle", &&self.0) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for FileHandle<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.0, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for FileHandle<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) - } - } - /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 - pub const SFTP_VERSION: u32 = 3; - /// The SFTP version of the client - pub struct InitVersionClient { - pub version: u32, - } - #[automatically_derived] - impl ::core::fmt::Debug for InitVersionClient { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "InitVersionClient", - "version", - &&self.version, - ) - } - } - impl ::sunset::sshwire::SSHEncode for InitVersionClient { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.version, s)?; - Ok(()) - } - } - impl<'de> ::sunset::sshwire::SSHDecode<'de> for InitVersionClient { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_version = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { version: field_version }) - } - } - /// The lowers SFTP version from the client and the server - pub struct InitVersionLowest { - pub version: u32, - } - #[automatically_derived] - impl ::core::fmt::Debug for InitVersionLowest { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "InitVersionLowest", - "version", - &&self.version, - ) - } - } - impl ::sunset::sshwire::SSHEncode for InitVersionLowest { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.version, s)?; - Ok(()) - } - } - impl<'de> ::sunset::sshwire::SSHDecode<'de> for InitVersionLowest { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_version = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { version: field_version }) - } - } - pub struct Open<'a> { - pub filename: Filename<'a>, - pub pflags: u32, - pub attrs: Attrs, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Open<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field3_finish( - f, - "Open", - "filename", - &self.filename, - "pflags", - &self.pflags, - "attrs", - &&self.attrs, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Open<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.filename, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.pflags, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.attrs, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Open<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_filename = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_pflags = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_attrs = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { - filename: field_filename, - pflags: field_pflags, - attrs: field_attrs, - }) - } - } - pub struct Close<'a> { - pub handle: FileHandle<'a>, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Close<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "Close", - "handle", - &&self.handle, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Close<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Close<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { handle: field_handle }) - } - } - pub struct Read<'a> { - pub handle: FileHandle<'a>, - pub offset: u64, - pub len: u32, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Read<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field3_finish( - f, - "Read", - "handle", - &self.handle, - "offset", - &self.offset, - "len", - &&self.len, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Read<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.offset, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.len, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Read<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_offset = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_len = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { - handle: field_handle, - offset: field_offset, - len: field_len, - }) - } - } - pub struct Write<'a> { - pub handle: FileHandle<'a>, - pub offset: u64, - pub data: BinString<'a>, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Write<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field3_finish( - f, - "Write", - "handle", - &self.handle, - "offset", - &self.offset, - "data", - &&self.data, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Write<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.offset, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.data, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Write<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_offset = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_data = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { - handle: field_handle, - offset: field_offset, - data: field_data, - }) - } - } - pub struct PathInfo<'a> { - pub path: TextString<'a>, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for PathInfo<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "PathInfo", - "path", - &&self.path, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for PathInfo<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.path, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for PathInfo<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_path = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { path: field_path }) - } - } - pub struct Status<'a> { - pub code: StatusCode, - pub message: TextString<'a>, - pub lang: TextString<'a>, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Status<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field3_finish( - f, - "Status", - "code", - &self.code, - "message", - &self.message, - "lang", - &&self.lang, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Status<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.code, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.message, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.lang, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Status<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_code = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_message = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_lang = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { - code: field_code, - message: field_message, - lang: field_lang, - }) - } - } - pub struct Handle<'a> { - pub handle: FileHandle<'a>, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Handle<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "Handle", - "handle", - &&self.handle, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Handle<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Handle<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { handle: field_handle }) - } - } - pub struct Data<'a> { - pub handle: FileHandle<'a>, - pub offset: u64, - pub data: BinString<'a>, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Data<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field3_finish( - f, - "Data", - "handle", - &self.handle, - "offset", - &self.offset, - "data", - &&self.data, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Data<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.offset, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.data, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Data<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_offset = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_data = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { - handle: field_handle, - offset: field_offset, - data: field_data, - }) - } - } - pub struct NameEntry<'a> { - pub filename: Filename<'a>, - /// longname is an undefined text line like "ls -l", - /// SHOULD NOT be used. - pub _longname: Filename<'a>, - pub attrs: Attrs, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for NameEntry<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field3_finish( - f, - "NameEntry", - "filename", - &self.filename, - "_longname", - &self._longname, - "attrs", - &&self.attrs, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for NameEntry<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.filename, s)?; - ::sunset::sshwire::SSHEncode::enc(&self._longname, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.attrs, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for NameEntry<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_filename = ::sunset::sshwire::SSHDecode::dec(s)?; - let field__longname = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_attrs = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { - filename: field_filename, - _longname: field__longname, - attrs: field_attrs, - }) - } - } - pub struct Name<'a>(pub Vec>); - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Name<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Name", &&self.0) - } - } - impl<'a: 'de, 'de> SSHDecode<'de> for Name<'a> - where - 'de: 'a, - { - fn dec(s: &mut S) -> WireResult - where - S: SSHSource<'de>, - { - let count = u32::dec(s)? as usize; - let mut names = Vec::with_capacity(count); - for _ in 0..count { - names.push(NameEntry::dec(s)?); - } - Ok(Name(names)) - } - } - impl<'a> SSHEncode for Name<'a> { - fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { - (self.0.len() as u32).enc(s)?; - for element in self.0.iter() { - element.enc(s)?; - } - Ok(()) - } - } - pub struct ResponseAttributes { - pub attrs: Attrs, - } - #[automatically_derived] - impl ::core::fmt::Debug for ResponseAttributes { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "ResponseAttributes", - "attrs", - &&self.attrs, - ) - } - } - impl ::sunset::sshwire::SSHEncode for ResponseAttributes { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.attrs, s)?; - Ok(()) - } - } - impl<'de> ::sunset::sshwire::SSHDecode<'de> for ResponseAttributes { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_attrs = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { attrs: field_attrs }) - } - } - pub struct ReqId(pub u32); - #[automatically_derived] - impl ::core::fmt::Debug for ReqId { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_tuple_field1_finish(f, "ReqId", &&self.0) - } - } - impl ::sunset::sshwire::SSHEncode for ReqId { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.0, s)?; - Ok(()) - } - } - impl<'de> ::sunset::sshwire::SSHDecode<'de> for ReqId { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) - } - } - #[automatically_derived] - impl ::core::clone::Clone for ReqId { - #[inline] - fn clone(&self) -> ReqId { - let _: ::core::clone::AssertParamIsClone; - *self - } - } - #[automatically_derived] - impl ::core::marker::Copy for ReqId {} - #[repr(u32)] - #[allow(non_camel_case_types)] - pub enum StatusCode { - #[sshwire(variant = "ssh_fx_ok")] - SSH_FX_OK = 0, - #[sshwire(variant = "ssh_fx_eof")] - SSH_FX_EOF = 1, - #[sshwire(variant = "ssh_fx_no_such_file")] - SSH_FX_NO_SUCH_FILE = 2, - #[sshwire(variant = "ssh_fx_permission_denied")] - SSH_FX_PERMISSION_DENIED = 3, - #[sshwire(variant = "ssh_fx_failure")] - SSH_FX_FAILURE = 4, - #[sshwire(variant = "ssh_fx_bad_message")] - SSH_FX_BAD_MESSAGE = 5, - #[sshwire(variant = "ssh_fx_no_connection")] - SSH_FX_NO_CONNECTION = 6, - #[sshwire(variant = "ssh_fx_connection_lost")] - SSH_FX_CONNECTION_LOST = 7, - #[sshwire(variant = "ssh_fx_unsupported")] - SSH_FX_OP_UNSUPPORTED = 8, - #[sshwire(unknown)] - #[num_enum(catch_all)] - Other(u32), - } - #[automatically_derived] - #[allow(non_camel_case_types)] - impl ::core::fmt::Debug for StatusCode { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - match self { - StatusCode::SSH_FX_OK => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_OK") - } - StatusCode::SSH_FX_EOF => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_EOF") - } - StatusCode::SSH_FX_NO_SUCH_FILE => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_NO_SUCH_FILE") - } - StatusCode::SSH_FX_PERMISSION_DENIED => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_PERMISSION_DENIED") - } - StatusCode::SSH_FX_FAILURE => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_FAILURE") - } - StatusCode::SSH_FX_BAD_MESSAGE => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_BAD_MESSAGE") - } - StatusCode::SSH_FX_NO_CONNECTION => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_NO_CONNECTION") - } - StatusCode::SSH_FX_CONNECTION_LOST => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_CONNECTION_LOST") - } - StatusCode::SSH_FX_OP_UNSUPPORTED => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_OP_UNSUPPORTED") - } - StatusCode::Other(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( - f, - "Other", - &__self_0, - ) - } - } - } - } - impl ::num_enum::FromPrimitive for StatusCode { - type Primitive = u32; - fn from_primitive(number: Self::Primitive) -> Self { - #![allow(non_upper_case_globals)] - const SSH_FX_OK__num_enum_0__: u32 = 0; - const SSH_FX_EOF__num_enum_0__: u32 = 1; - const SSH_FX_NO_SUCH_FILE__num_enum_0__: u32 = 2; - const SSH_FX_PERMISSION_DENIED__num_enum_0__: u32 = 3; - const SSH_FX_FAILURE__num_enum_0__: u32 = 4; - const SSH_FX_BAD_MESSAGE__num_enum_0__: u32 = 5; - const SSH_FX_NO_CONNECTION__num_enum_0__: u32 = 6; - const SSH_FX_CONNECTION_LOST__num_enum_0__: u32 = 7; - const SSH_FX_OP_UNSUPPORTED__num_enum_0__: u32 = 8; - #[deny(unreachable_patterns)] - match number { - SSH_FX_OK__num_enum_0__ => Self::SSH_FX_OK, - SSH_FX_EOF__num_enum_0__ => Self::SSH_FX_EOF, - SSH_FX_NO_SUCH_FILE__num_enum_0__ => Self::SSH_FX_NO_SUCH_FILE, - SSH_FX_PERMISSION_DENIED__num_enum_0__ => Self::SSH_FX_PERMISSION_DENIED, - SSH_FX_FAILURE__num_enum_0__ => Self::SSH_FX_FAILURE, - SSH_FX_BAD_MESSAGE__num_enum_0__ => Self::SSH_FX_BAD_MESSAGE, - SSH_FX_NO_CONNECTION__num_enum_0__ => Self::SSH_FX_NO_CONNECTION, - SSH_FX_CONNECTION_LOST__num_enum_0__ => Self::SSH_FX_CONNECTION_LOST, - SSH_FX_OP_UNSUPPORTED__num_enum_0__ => Self::SSH_FX_OP_UNSUPPORTED, - #[allow(unreachable_patterns)] - _ => Self::Other(number), - } - } - } - impl ::core::convert::From for StatusCode { - #[inline] - fn from(number: u32) -> Self { - ::num_enum::FromPrimitive::from_primitive(number) - } - } - #[doc(hidden)] - impl ::num_enum::CannotDeriveBothFromPrimitiveAndTryFromPrimitive for StatusCode {} - impl ::sunset::sshwire::SSHEncode for StatusCode { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - match *self { - Self::SSH_FX_OK => {} - Self::SSH_FX_EOF => {} - Self::SSH_FX_NO_SUCH_FILE => {} - Self::SSH_FX_PERMISSION_DENIED => {} - Self::SSH_FX_FAILURE => {} - Self::SSH_FX_BAD_MESSAGE => {} - Self::SSH_FX_NO_CONNECTION => {} - Self::SSH_FX_CONNECTION_LOST => {} - Self::SSH_FX_OP_UNSUPPORTED => {} - Self::Other(ref i) => { - return Err(::sunset::sshwire::WireError::UnknownVariant); - } - } - #[allow(unreachable_code)] Ok(()) - } - } - impl ::sunset::sshwire::SSHEncodeEnum for StatusCode { - fn variant_name(&self) -> ::sunset::sshwire::WireResult<&'static str> { - let r = match self { - Self::SSH_FX_OK => "ssh_fx_ok", - Self::SSH_FX_EOF => "ssh_fx_eof", - Self::SSH_FX_NO_SUCH_FILE => "ssh_fx_no_such_file", - Self::SSH_FX_PERMISSION_DENIED => "ssh_fx_permission_denied", - Self::SSH_FX_FAILURE => "ssh_fx_failure", - Self::SSH_FX_BAD_MESSAGE => "ssh_fx_bad_message", - Self::SSH_FX_NO_CONNECTION => "ssh_fx_no_connection", - Self::SSH_FX_CONNECTION_LOST => "ssh_fx_connection_lost", - Self::SSH_FX_OP_UNSUPPORTED => "ssh_fx_unsupported", - Self::Other(_) => { - return Err(::sunset::sshwire::WireError::UnknownVariant); - } - }; - #[allow(unreachable_code)] Ok(r) - } - } - impl<'de> SSHDecode<'de> for StatusCode { - fn dec(s: &mut S) -> WireResult - where - S: SSHSource<'de>, - { - Ok(StatusCode::from(u32::dec(s)?)) - } - } - pub struct ExtPair<'a> { - pub name: &'a str, - pub data: BinString<'a>, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for ExtPair<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field2_finish( - f, - "ExtPair", - "name", - &self.name, - "data", - &&self.data, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for ExtPair<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.name, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.data, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for ExtPair<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_name = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_data = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { - name: field_name, - data: field_data, - }) - } - } - /// Files attributes to describe Files as SFTP v3 specification - pub struct Attrs { - pub size: Option, - pub uid: Option, - pub gid: Option, - pub permissions: Option, - pub atime: Option, - pub mtime: Option, - pub ext_count: Option, - } - #[automatically_derived] - impl ::core::fmt::Debug for Attrs { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - let names: &'static _ = &[ - "size", - "uid", - "gid", - "permissions", - "atime", - "mtime", - "ext_count", - ]; - let values: &[&dyn ::core::fmt::Debug] = &[ - &self.size, - &self.uid, - &self.gid, - &self.permissions, - &self.atime, - &self.mtime, - &&self.ext_count, - ]; - ::core::fmt::Formatter::debug_struct_fields_finish(f, "Attrs", names, values) - } - } - #[automatically_derived] - impl ::core::default::Default for Attrs { - #[inline] - fn default() -> Attrs { - Attrs { - size: ::core::default::Default::default(), - uid: ::core::default::Default::default(), - gid: ::core::default::Default::default(), - permissions: ::core::default::Default::default(), - atime: ::core::default::Default::default(), - mtime: ::core::default::Default::default(), - ext_count: ::core::default::Default::default(), - } - } - } - #[repr(u32)] - #[allow(non_camel_case_types)] - pub enum AttrsFlags { - SSH_FILEXFER_ATTR_SIZE = 0x01, - SSH_FILEXFER_ATTR_UIDGID = 0x02, - SSH_FILEXFER_ATTR_PERMISSIONS = 0x04, - SSH_FILEXFER_ATTR_ACMODTIME = 0x08, - SSH_FILEXFER_ATTR_EXTENDED = 0x80000000, - } - impl core::ops::AddAssign for u32 { - fn add_assign(&mut self, other: AttrsFlags) { - *self |= other as u32; - } - } - impl core::ops::BitAnd for u32 { - type Output = u32; - fn bitand(self, rhs: AttrsFlags) -> Self::Output { - self & rhs as u32 - } - } - impl Attrs { - pub fn flags(&self) -> u32 { - let mut flags: u32 = 0; - if self.size.is_some() { - flags += AttrsFlags::SSH_FILEXFER_ATTR_SIZE; - } - if self.uid.is_some() || self.gid.is_some() { - flags += AttrsFlags::SSH_FILEXFER_ATTR_UIDGID; - } - if self.permissions.is_some() { - flags += AttrsFlags::SSH_FILEXFER_ATTR_PERMISSIONS; - } - if self.atime.is_some() || self.mtime.is_some() { - flags += AttrsFlags::SSH_FILEXFER_ATTR_ACMODTIME; - } - flags - } - } - impl SSHEncode for Attrs { - fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { - self.flags().enc(s)?; - if let Some(value) = self.size.as_ref() { - value.enc(s)? - } - if let Some(value) = self.uid.as_ref() { - value.enc(s)? - } - if let Some(value) = self.gid.as_ref() { - value.enc(s)? - } - if let Some(value) = self.permissions.as_ref() { - value.enc(s)? - } - if let Some(value) = self.atime.as_ref() { - value.enc(s)? - } - if let Some(value) = self.mtime.as_ref() { - value.enc(s)? - } - Ok(()) - } - } - impl<'de> SSHDecode<'de> for Attrs { - fn dec(s: &mut S) -> WireResult - where - S: SSHSource<'de>, - { - let mut attrs = Attrs::default(); - let flags = u32::dec(s)? as u32; - if flags & AttrsFlags::SSH_FILEXFER_ATTR_SIZE != 0 { - attrs.size = Some(u64::dec(s)?); - } - if flags & AttrsFlags::SSH_FILEXFER_ATTR_UIDGID != 0 { - attrs.uid = Some(u32::dec(s)?); - attrs.gid = Some(u32::dec(s)?); - } - if flags & AttrsFlags::SSH_FILEXFER_ATTR_PERMISSIONS != 0 { - attrs.permissions = Some(u32::dec(s)?); - } - if flags & AttrsFlags::SSH_FILEXFER_ATTR_ACMODTIME != 0 { - attrs.atime = Some(u32::dec(s)?); - attrs.mtime = Some(u32::dec(s)?); - } - Ok(attrs) - } - } - /// Represent a subset of the SFTP packet types defined by draft-ietf-secsh-filexfer-02 - #[repr(u8)] - #[allow(non_camel_case_types)] - pub enum SftpNum { - #[sshwire(variant = "ssh_fxp_init")] - SSH_FXP_INIT = 1, - #[sshwire(variant = "ssh_fxp_version")] - SSH_FXP_VERSION = 2, - #[sshwire(variant = "ssh_fxp_open")] - SSH_FXP_OPEN = 3, - #[sshwire(variant = "ssh_fxp_close")] - SSH_FXP_CLOSE = 4, - #[sshwire(variant = "ssh_fxp_read")] - SSH_FXP_READ = 5, - #[sshwire(variant = "ssh_fxp_write")] - SSH_FXP_WRITE = 6, - #[sshwire(variant = "ssh_fxp_realpath")] - SSH_FXP_REALPATH = 16, - #[sshwire(variant = "ssh_fxp_status")] - SSH_FXP_STATUS = 101, - #[sshwire(variant = "ssh_fxp_handle")] - SSH_FXP_HANDLE = 102, - #[sshwire(variant = "ssh_fxp_data")] - SSH_FXP_DATA = 103, - #[sshwire(variant = "ssh_fxp_name")] - SSH_FXP_NAME = 104, - #[sshwire(unknown)] - #[num_enum(catch_all)] - Other(u8), - } - #[automatically_derived] - #[allow(non_camel_case_types)] - impl ::core::fmt::Debug for SftpNum { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - match self { - SftpNum::SSH_FXP_INIT => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_INIT") - } - SftpNum::SSH_FXP_VERSION => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_VERSION") - } - SftpNum::SSH_FXP_OPEN => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_OPEN") - } - SftpNum::SSH_FXP_CLOSE => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_CLOSE") - } - SftpNum::SSH_FXP_READ => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_READ") - } - SftpNum::SSH_FXP_WRITE => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_WRITE") - } - SftpNum::SSH_FXP_REALPATH => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_REALPATH") - } - SftpNum::SSH_FXP_STATUS => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_STATUS") - } - SftpNum::SSH_FXP_HANDLE => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_HANDLE") - } - SftpNum::SSH_FXP_DATA => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_DATA") - } - SftpNum::SSH_FXP_NAME => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_NAME") - } - SftpNum::Other(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( - f, - "Other", - &__self_0, - ) - } - } - } - } - #[automatically_derived] - #[allow(non_camel_case_types)] - impl ::core::clone::Clone for SftpNum { - #[inline] - fn clone(&self) -> SftpNum { - match self { - SftpNum::SSH_FXP_INIT => SftpNum::SSH_FXP_INIT, - SftpNum::SSH_FXP_VERSION => SftpNum::SSH_FXP_VERSION, - SftpNum::SSH_FXP_OPEN => SftpNum::SSH_FXP_OPEN, - SftpNum::SSH_FXP_CLOSE => SftpNum::SSH_FXP_CLOSE, - SftpNum::SSH_FXP_READ => SftpNum::SSH_FXP_READ, - SftpNum::SSH_FXP_WRITE => SftpNum::SSH_FXP_WRITE, - SftpNum::SSH_FXP_REALPATH => SftpNum::SSH_FXP_REALPATH, - SftpNum::SSH_FXP_STATUS => SftpNum::SSH_FXP_STATUS, - SftpNum::SSH_FXP_HANDLE => SftpNum::SSH_FXP_HANDLE, - SftpNum::SSH_FXP_DATA => SftpNum::SSH_FXP_DATA, - SftpNum::SSH_FXP_NAME => SftpNum::SSH_FXP_NAME, - SftpNum::Other(__self_0) => { - SftpNum::Other(::core::clone::Clone::clone(__self_0)) - } - } - } - } - impl ::num_enum::FromPrimitive for SftpNum { - type Primitive = u8; - fn from_primitive(number: Self::Primitive) -> Self { - #![allow(non_upper_case_globals)] - const SSH_FXP_INIT__num_enum_0__: u8 = 1; - const SSH_FXP_VERSION__num_enum_0__: u8 = 2; - const SSH_FXP_OPEN__num_enum_0__: u8 = 3; - const SSH_FXP_CLOSE__num_enum_0__: u8 = 4; - const SSH_FXP_READ__num_enum_0__: u8 = 5; - const SSH_FXP_WRITE__num_enum_0__: u8 = 6; - const SSH_FXP_REALPATH__num_enum_0__: u8 = 16; - const SSH_FXP_STATUS__num_enum_0__: u8 = 101; - const SSH_FXP_HANDLE__num_enum_0__: u8 = 102; - const SSH_FXP_DATA__num_enum_0__: u8 = 103; - const SSH_FXP_NAME__num_enum_0__: u8 = 104; - #[deny(unreachable_patterns)] - match number { - SSH_FXP_INIT__num_enum_0__ => Self::SSH_FXP_INIT, - SSH_FXP_VERSION__num_enum_0__ => Self::SSH_FXP_VERSION, - SSH_FXP_OPEN__num_enum_0__ => Self::SSH_FXP_OPEN, - SSH_FXP_CLOSE__num_enum_0__ => Self::SSH_FXP_CLOSE, - SSH_FXP_READ__num_enum_0__ => Self::SSH_FXP_READ, - SSH_FXP_WRITE__num_enum_0__ => Self::SSH_FXP_WRITE, - SSH_FXP_REALPATH__num_enum_0__ => Self::SSH_FXP_REALPATH, - SSH_FXP_STATUS__num_enum_0__ => Self::SSH_FXP_STATUS, - SSH_FXP_HANDLE__num_enum_0__ => Self::SSH_FXP_HANDLE, - SSH_FXP_DATA__num_enum_0__ => Self::SSH_FXP_DATA, - SSH_FXP_NAME__num_enum_0__ => Self::SSH_FXP_NAME, - #[allow(unreachable_patterns)] - _ => Self::Other(number), - } - } - } - impl ::core::convert::From for SftpNum { - #[inline] - fn from(number: u8) -> Self { - ::num_enum::FromPrimitive::from_primitive(number) - } - } - #[doc(hidden)] - impl ::num_enum::CannotDeriveBothFromPrimitiveAndTryFromPrimitive for SftpNum {} - impl ::sunset::sshwire::SSHEncode for SftpNum { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - match *self { - Self::SSH_FXP_INIT => {} - Self::SSH_FXP_VERSION => {} - Self::SSH_FXP_OPEN => {} - Self::SSH_FXP_CLOSE => {} - Self::SSH_FXP_READ => {} - Self::SSH_FXP_WRITE => {} - Self::SSH_FXP_REALPATH => {} - Self::SSH_FXP_STATUS => {} - Self::SSH_FXP_HANDLE => {} - Self::SSH_FXP_DATA => {} - Self::SSH_FXP_NAME => {} - Self::Other(ref i) => { - return Err(::sunset::sshwire::WireError::UnknownVariant); - } - } - #[allow(unreachable_code)] Ok(()) - } - } - impl ::sunset::sshwire::SSHEncodeEnum for SftpNum { - fn variant_name(&self) -> ::sunset::sshwire::WireResult<&'static str> { - let r = match self { - Self::SSH_FXP_INIT => "ssh_fxp_init", - Self::SSH_FXP_VERSION => "ssh_fxp_version", - Self::SSH_FXP_OPEN => "ssh_fxp_open", - Self::SSH_FXP_CLOSE => "ssh_fxp_close", - Self::SSH_FXP_READ => "ssh_fxp_read", - Self::SSH_FXP_WRITE => "ssh_fxp_write", - Self::SSH_FXP_REALPATH => "ssh_fxp_realpath", - Self::SSH_FXP_STATUS => "ssh_fxp_status", - Self::SSH_FXP_HANDLE => "ssh_fxp_handle", - Self::SSH_FXP_DATA => "ssh_fxp_data", - Self::SSH_FXP_NAME => "ssh_fxp_name", - Self::Other(_) => { - return Err(::sunset::sshwire::WireError::UnknownVariant); - } - }; - #[allow(unreachable_code)] Ok(r) - } - } - impl<'de> SSHDecode<'de> for SftpNum { - fn dec(s: &mut S) -> WireResult - where - S: SSHSource<'de>, - { - Ok(SftpNum::from(u8::dec(s)?)) - } - } - impl From for u8 { - fn from(sftp_num: SftpNum) -> u8 { - match sftp_num { - SftpNum::SSH_FXP_INIT => 1, - SftpNum::SSH_FXP_VERSION => 2, - SftpNum::SSH_FXP_OPEN => 3, - SftpNum::SSH_FXP_CLOSE => 4, - SftpNum::SSH_FXP_READ => 5, - SftpNum::SSH_FXP_WRITE => 6, - SftpNum::SSH_FXP_REALPATH => 16, - SftpNum::SSH_FXP_STATUS => 101, - SftpNum::SSH_FXP_HANDLE => 102, - SftpNum::SSH_FXP_DATA => 103, - SftpNum::SSH_FXP_NAME => 104, - SftpNum::Other(number) => number, - } - } - } - impl SftpNum { - fn is_init(&self) -> bool { - (1..=1).contains(&(u8::from(self.clone()))) - } - fn is_request(&self) -> bool { - (2..=99).contains(&(u8::from(self.clone()))) - } - fn is_response(&self) -> bool { - (100..=199).contains(&(u8::from(self.clone()))) - } - } - /// Top level SSH packet enum - /// - /// It helps identifying the SFTP Packet type and handling it accordingly - /// This is done using the SFTP field type - pub enum SftpPacket<'a> { - Init(InitVersionClient), - Version(InitVersionLowest), - Open(ReqId, Open<'a>), - Close(ReqId, Close<'a>), - Read(ReqId, Read<'a>), - Write(ReqId, Write<'a>), - PathInfo(ReqId, PathInfo<'a>), - Status(ReqId, Status<'a>), - Handle(ReqId, Handle<'a>), - Data(ReqId, Data<'a>), - Name(ReqId, Name<'a>), - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for SftpPacket<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - match self { - SftpPacket::Init(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( - f, - "Init", - &__self_0, - ) - } - SftpPacket::Version(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( - f, - "Version", - &__self_0, - ) - } - SftpPacket::Open(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Open", - __self_0, - &__self_1, - ) - } - SftpPacket::Close(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Close", - __self_0, - &__self_1, - ) - } - SftpPacket::Read(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Read", - __self_0, - &__self_1, - ) - } - SftpPacket::Write(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Write", - __self_0, - &__self_1, - ) - } - SftpPacket::PathInfo(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "PathInfo", - __self_0, - &__self_1, - ) - } - SftpPacket::Status(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Status", - __self_0, - &__self_1, - ) - } - SftpPacket::Handle(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Handle", - __self_0, - &__self_1, - ) - } - SftpPacket::Data(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Data", - __self_0, - &__self_1, - ) - } - SftpPacket::Name(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Name", - __self_0, - &__self_1, - ) - } - } - } - } - impl SSHEncode for SftpPacket<'_> { - fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { - let t = u8::from(self.sftp_num()); - t.enc(s)?; - match self { - SftpPacket::Init(p) => p.enc(s)?, - SftpPacket::Version(p) => p.enc(s)?, - SftpPacket::Open(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::Close(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::Read(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::Write(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::PathInfo(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::Status(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::Handle(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::Data(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::Name(id, p) => { - id.enc(s)?; - p.enc(s)? - } - }; - Ok(()) - } - } - impl<'a: 'de, 'de> SSHDecode<'de> for SftpPacket<'a> - where - 'de: 'a, - { - fn dec(s: &mut S) -> WireResult - where - S: SSHSource<'de>, - { - let packet_type_number = u8::dec(s)?; - let packet_type = SftpNum::from(packet_type_number); - let decoded_packet = match packet_type { - SftpNum::SSH_FXP_INIT => { - let inner_type = ::dec(s)?; - SftpPacket::Init(inner_type) - } - SftpNum::SSH_FXP_VERSION => { - let inner_type = ::dec(s)?; - SftpPacket::Version(inner_type) - } - SftpNum::SSH_FXP_OPEN => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Open(req_id, inner_type) - } - SftpNum::SSH_FXP_CLOSE => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Close(req_id, inner_type) - } - SftpNum::SSH_FXP_READ => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Read(req_id, inner_type) - } - SftpNum::SSH_FXP_WRITE => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Write(req_id, inner_type) - } - SftpNum::SSH_FXP_REALPATH => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::PathInfo(req_id, inner_type) - } - SftpNum::SSH_FXP_STATUS => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Status(req_id, inner_type) - } - SftpNum::SSH_FXP_HANDLE => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Handle(req_id, inner_type) - } - SftpNum::SSH_FXP_DATA => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Data(req_id, inner_type) - } - SftpNum::SSH_FXP_NAME => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Name(req_id, inner_type) - } - _ => { - return Err(WireError::UnknownPacket { - number: packet_type_number, - }); - } - }; - Ok(decoded_packet) - } - } - impl<'a> SftpPacket<'a> { - /// Maps `SpecificPacketVariant` to `message_num` - pub fn sftp_num(&self) -> SftpNum { - match self { - SftpPacket::Init(_) => SftpNum::from(1 as u8), - SftpPacket::Version(_) => SftpNum::from(2 as u8), - SftpPacket::Open(_, _) => SftpNum::from(3 as u8), - SftpPacket::Close(_, _) => SftpNum::from(4 as u8), - SftpPacket::Read(_, _) => SftpNum::from(5 as u8), - SftpPacket::Write(_, _) => SftpNum::from(6 as u8), - SftpPacket::PathInfo(_, _) => SftpNum::from(16 as u8), - SftpPacket::Status(_, _) => SftpNum::from(101 as u8), - SftpPacket::Handle(_, _) => SftpNum::from(102 as u8), - SftpPacket::Data(_, _) => SftpNum::from(103 as u8), - SftpPacket::Name(_, _) => SftpNum::from(104 as u8), - } - } - /// Encode a request. - /// - /// Used by a SFTP client. Does not include the length field. - pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { - if !self.sftp_num().is_request() { - return Err(WireError::PacketWrong); - } - self.sftp_num().enc(s)?; - id.0.enc(s)?; - self.enc(s) - } - /// Decode a response. - /// - /// Used by a SFTP client. Does not include the length field. - pub fn decode_response<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> - where - S: SSHSource<'de>, - 'a: 'de, - 'de: 'a, - { - let num = SftpNum::from(u8::dec(s)?); - if !num.is_response() { - return Err(WireError::PacketWrong); - } - let id = ReqId(u32::dec(s)?); - Ok((id, Self::dec(s)?)) - } - /// Decode a request. Includes Init - /// - /// Used by a SFTP server. Does not include the length field. If the request does not have id (Initialisation) - pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(Option, Self)> - where - S: SSHSource<'de>, - 'a: 'de, - 'de: 'a, - { - let num = SftpNum::from(u8::dec(s)?); - if (!num.is_request() && !num.is_init()) { - return Err(WireError::PacketWrong); - } - let maybe_id = if num.is_init() { None } else { Some(ReqId(u32::dec(s)?)) }; - let sftp_packet = Self::dec(s)?; - Ok((maybe_id, sftp_packet)) - } - /// Encode a response. - /// - /// Used by a SFTP server. Does not include the length field. - pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { - if !self.sftp_num().is_response() { - return Err(WireError::PacketWrong); - } - self.sftp_num().enc(s)?; - id.0.enc(s)?; - self.enc(s) - } - } - impl<'a> From for SftpPacket<'a> { - fn from(s: InitVersionClient) -> SftpPacket<'a> { - SftpPacket::Init(s) - } - } - impl<'a> From for SftpPacket<'a> { - fn from(s: InitVersionLowest) -> SftpPacket<'a> { - SftpPacket::Version(s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Open<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_open", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Open(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Close<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_close", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Close(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Read<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_read", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Read(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Write<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_write", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Write(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: PathInfo<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_realpath", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::PathInfo(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Status<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_status", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Status(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Handle<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_handle", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Handle(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Data<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_data", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Data(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Name<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_name", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Name(ReqId(0), s) - } - } -} -mod sftpserver { - use crate::proto::{Attrs, StatusCode}; - use core::marker::PhantomData; - pub type Result = core::result::Result; - /// All trait functions are optional in the SFTP protocol. - /// Some less core operations have a Provided implementation returning - /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, - /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. - pub trait SftpServer { - type Handle; - async fn open(filename: &str, flags: u32, attrs: &Attrs) -> Result; - /// Close either a file or directory handle - async fn close(handle: &Self::Handle) -> Result<()>; - async fn read( - handle: &Self::Handle, - offset: u64, - reply: &mut ReadReply, - ) -> Result<()>; - async fn write(handle: &Self::Handle, offset: u64, buf: &[u8]) -> Result<()>; - async fn opendir(dir: &str) -> Result; - async fn readdir(handle: &Self::Handle, reply: &mut DirReply) -> Result<()>; - } - pub struct ReadReply<'g, 'a> { - chan: ChanOut<'g, 'a>, - } - impl<'g, 'a> ReadReply<'g, 'a> { - pub async fn reply(self, data: &[u8]) {} - } - pub struct DirReply<'g, 'a> { - chan: ChanOut<'g, 'a>, - } - impl<'g, 'a> DirReply<'g, 'a> { - pub async fn reply(self, data: &[u8]) {} - } - pub struct ChanOut<'g, 'a> { - _phantom_g: PhantomData<&'g ()>, - _phantom_a: PhantomData<&'a ()>, - } -} -pub use sftpserver::DirReply; -pub use sftpserver::ReadReply; -pub use sftpserver::Result; -pub use sftpserver::SftpServer; -pub use proto::Attrs; -pub use proto::SFTP_VERSION; -pub use proto::SftpNum; -pub use proto::SftpPacket; From 92b170b5ea2cf3d9c9b21b4794a7525f043166bc Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 23 Sep 2025 20:08:56 +1000 Subject: [PATCH 106/393] Bact to SftpHandle: Process failing as soon as possible on short packets --- sftp/src/sftphandle.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 0d2357c0..43619ce7 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -82,6 +82,12 @@ where let in_len = buffer_in.len(); debug!("Received {:} bytes to process", in_len); + if self.partial_write_request_tracker.is_none() + & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) + { + return Err(WireError::PacketWrong); + } + let mut source = SftpSource::new(buffer_in); trace!("Source content: {:?}", source); @@ -151,12 +157,6 @@ where return Ok(sink.finalize()); } - if self.partial_write_request_tracker.is_none() - & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) - { - return Err(WireError::PacketWrong); - } - let packet_length = u32::dec(&mut source)?; trace!("Packet field length content: {}", packet_length); From 13487a60f68125573336ac643e0dc36f960666b5 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 24 Sep 2025 09:35:05 +1000 Subject: [PATCH 107/393] Added extra docs to SFTP proto decode_request --- sftp/src/proto.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 01abe820..b5bae2e2 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -609,7 +609,7 @@ macro_rules! sftpmessages { /// /// Used by a SFTP server. Does not include the length field. /// - /// It will fail if the received packet is a response + /// It will fail if the received packet is a response, no valid or incomplete packet pub fn decode_request<'de, S>(s: &mut S) -> WireResult where S: SSHSource<'de>, @@ -617,7 +617,6 @@ macro_rules! sftpmessages { 'de: 'a { - // let sftp_packet = Self::dec(s)?; match Self::dec(s) { Ok(sftp_packet)=> { if (!sftp_packet.sftp_num().is_request() From a9934a0b6df56707c92712a2953a850afdaabaa5 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 24 Sep 2025 20:11:55 +1000 Subject: [PATCH 108/393] Broken: Ugly experimentation with multi part consecutive packets Somehow I am sending an ok different from the expected by the client --- sftp/src/proto.rs | 3 +- sftp/src/sftphandle.rs | 242 ++++++++++++++++++++--------------------- 2 files changed, 121 insertions(+), 124 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index b5bae2e2..5a709e42 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -616,7 +616,8 @@ macro_rules! sftpmessages { 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes 'de: 'a { - + let packet_length = u32::dec(s)?; + debug!("Packet field len = {:?}. Source len = {:?}", packet_length, s.remaining()); match Self::dec(s) { Ok(sftp_packet)=> { if (!sftp_packet.sftp_num().is_request() diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 43619ce7..c7949ee2 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,13 +1,13 @@ use crate::OpaqueFileHandle; use crate::proto::{ self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, - SftpPacket, Status, StatusCode, + SftpPacket, Status, StatusCode, Write, }; use crate::sftpserver::SftpServer; use crate::sftpsink::SftpSink; use crate::sftpsource::SftpSource; -use sunset::sshwire::{SSHDecode, WireError, WireResult}; +use sunset::sshwire::{WireError, WireResult}; use core::u32; #[allow(unused_imports)] @@ -38,7 +38,7 @@ impl PartialWriteRequestTracker { }) } - pub fn get_file_handle(&self) -> T { + pub fn get_opaque_file_handle(&self) -> T { self.obscure_file_handle.clone() } } @@ -81,7 +81,6 @@ where ) -> WireResult { let in_len = buffer_in.len(); debug!("Received {:} bytes to process", in_len); - if self.partial_write_request_tracker.is_none() & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) { @@ -94,149 +93,146 @@ where let mut sink = SftpSink::new(buffer_out); if let Some(mut write_tracker) = self.partial_write_request_tracker.take() { - trace!( + debug!( "Processing successive chunks of a long write packet. Stored data: {:?}", write_tracker ); + let opaque_handle = write_tracker.get_opaque_file_handle(); + + let partial_write = { + let usable_data = in_len.min(write_tracker.remain_data_len as usize); + + if in_len > write_tracker.remain_data_len as usize { + // This is because we are receiving one packet and the beginning of the next one + // let sftp_num = source.peak_packet_type_with_offset( + // write_tracker.remain_data_len as usize, + // )?; + // error!( + // "There is too much data in the buffer! {:?} > than expected {:?}. There is a trailing packet in buffer: {:?}", + // in_len, write_tracker.remain_data_len, sftp_num + // ); + }; - let usable_data = in_len.min(write_tracker.remain_data_len as usize); + let data_segment = source.dec_as_binstring(usable_data)?; - if in_len > write_tracker.remain_data_len as usize { - // This is because we are receiving one packet and the beginning of the next one - let sftp_num = source.peak_packet_type_with_offset( - write_tracker.remain_data_len as usize, - )?; - error!( - "There is too much data in the buffer! {:?} > than expected {:?}. There is a trailing packet in buffer: {:?}", - in_len, write_tracker.remain_data_len, sftp_num - ); - }; + // TODO: Do proper casting with checks u32::try_from(data_in_buffer.0.len()) + let data_segment_len = data_segment.0.len() as u32; - let data_segment = source.dec_as_binstring(usable_data)?; + let current_write_offset = write_tracker.remain_data_offset; + write_tracker.remain_data_offset += data_segment_len as u64; + write_tracker.remain_data_len -= data_segment_len; - // TODO: Do proper casting with checks u32::try_from(data_in_buffer.0.len()) - let data_segment_len = data_segment.0.len() as u32; + let obscure_file_handle = write_tracker.get_opaque_file_handle(); + debug!( + "Processing successive chunks of a long write packet. Writing : obscure_file_handle = {:?}, write_offset = {:?}, data_segment = {:?}, data remaining = {:?}", + obscure_file_handle, + current_write_offset, + data_segment, + write_tracker.remain_data_len + ); - let current_write_offset = write_tracker.remain_data_offset; - write_tracker.remain_data_offset += data_segment_len as u64; - write_tracker.remain_data_len -= data_segment_len; + let part_sftp_packet_write_request = SftpPacket::Write( + write_tracker.req_id, + Write { + handle: opaque_handle.into_file_handle(), + offset: current_write_offset, + data: data_segment, + }, + ); - let obscure_file_handle = write_tracker.get_file_handle(); + part_sftp_packet_write_request + }; + + self.process_known_request(&mut sink, partial_write).await?; debug!( - "Processing successive chunks of a long write packet. Writing : obscure_file_handle = {:?}, write_offset = {:?}, data_segment = {:?}, data remaining = {:?}", - obscure_file_handle, - current_write_offset, - data_segment, - write_tracker.remain_data_len + "Finishing partial_write process. write_tracker = {:?}", + write_tracker ); - match self.file_server.write( - &obscure_file_handle, - current_write_offset, - data_segment.as_ref(), - ) { - Ok(_) => { - if write_tracker.remain_data_len > 0 { - self.partial_write_request_tracker = Some(write_tracker); - } else { - push_ok(write_tracker.req_id, &mut sink)?; - info!("Finished multi part Write Request"); - self.partial_write_request_tracker = None; // redundant - } + if write_tracker.remain_data_len > 0 { + self.partial_write_request_tracker = Some(write_tracker); + } else { + push_ok(write_tracker.req_id, &mut sink)?; + info!("Finished multi part Write Request"); + self.partial_write_request_tracker = None; // redundant + } + } else { + match SftpPacket::decode_request(&mut source) { + Ok(request) => { + info!("received request: {:?}", request); + self.process_known_request(&mut sink, request).await?; } Err(e) => { - self.partial_write_request_tracker = None; - error!("SFTP write thrown: {:?}", e); - push_general_failure( - write_tracker.req_id, - "error writing", - &mut sink, - )?; - } - }; - - return Ok(sink.finalize()); - } - - let packet_length = u32::dec(&mut source)?; - trace!("Packet field length content: {}", packet_length); - - match SftpPacket::decode_request(&mut source) { - Ok(request) => { - info!("received request: {:?}", request); - self.process_known_request(&mut sink, request).await?; - } - Err(e) => { - match e { - WireError::RanOut => { - warn!( - "RanOut for the SFTP Packet in the source buffer: {:?}", - e - ); - // let packet_total_length = source.peak_packet_len()?; - let packet_type = source.peak_packet_type()?; - match packet_type { - SftpNum::SSH_FXP_WRITE => { - let ( + match e { + WireError::RanOut => { + warn!( + "RanOut for the SFTP Packet in the source buffer: {:?}", + e + ); + // let packet_total_length = source.peak_packet_len()?; + let packet_type = source.peak_packet_type()?; + match packet_type { + SftpNum::SSH_FXP_WRITE => { + let ( file_handle, req_id, offset, data_in_buffer, write_tracker, ) = source - .get_packet_partial_write_content_and_tracker( - )?; - debug!( - "Packet is too long for the source buffer, will write what we have now and continue writing later" - ); - trace!( - "handle = {:?}, req_id = {:?}, offset = {:?}, data_in_buffer = {:?}, write_tracker = {:?}", - file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle - req_id, - offset, - data_in_buffer, - write_tracker - ); - - match self.file_server.write( - &file_handle, - offset, - data_in_buffer.as_ref(), - ) { - Ok(_) => { - self.partial_write_request_tracker = - Some(write_tracker); - } - Err(e) => { - error!("SFTP write thrown: {:?}", e); - push_general_failure( - req_id, - "error writing ", - &mut sink, - )?; - } - }; - } - _ => { - push_general_failure( - ReqId(u32::MAX), - "Unsupported Request: Too long", - &mut sink, + .get_packet_partial_write_content_and_tracker( )?; - } - }; - } - WireError::UnknownPacket { number: _ } => { - warn!("Error decoding SFTP Packet:{:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; - } - _ => { - error!("Error decoding SFTP Packet: {:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; + debug!( + "Packet is too long for the source buffer, will write what we have now and continue writing later" + ); + trace!( + "handle = {:?}, req_id = {:?}, offset = {:?}, data_in_buffer = {:?}, write_tracker = {:?}", + file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle + req_id, + offset, + data_in_buffer, + write_tracker + ); + + match self.file_server.write( + &file_handle, + offset, + data_in_buffer.as_ref(), + ) { + Ok(_) => { + self.partial_write_request_tracker = + Some(write_tracker); + } + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure( + req_id, + "error writing ", + &mut sink, + )?; + } + }; + } + _ => { + push_general_failure( + ReqId(u32::MAX), + "Unsupported Request: Too long", + &mut sink, + )?; + } + }; + } + WireError::UnknownPacket { number: _ } => { + warn!("Error decoding SFTP Packet:{:?}", e); + push_unsupported(ReqId(u32::MAX), &mut sink)?; + } + _ => { + error!("Error decoding SFTP Packet: {:?}", e); + push_unsupported(ReqId(u32::MAX), &mut sink)?; + } } } } }; - Ok(sink.finalize()) } From 511a63ca2a0fa631977a95a34b4cb5b49c62ca0e Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 24 Sep 2025 20:27:10 +1000 Subject: [PATCH 109/393] decoding request len in SftpPacket::decode_request --- sftp/src/proto.rs | 2 ++ sftp/src/sftphandle.rs | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index b5bae2e2..0ed4bea0 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -616,6 +616,8 @@ macro_rules! sftpmessages { 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes 'de: 'a { + let packet_length = u32::dec(s)?; + trace!("Packet field len = {:?}, buffer len = {:?}", packet_length, s.remaining()); match Self::dec(s) { Ok(sftp_packet)=> { diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 43619ce7..b2746120 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -157,9 +157,6 @@ where return Ok(sink.finalize()); } - let packet_length = u32::dec(&mut source)?; - trace!("Packet field length content: {}", packet_length); - match SftpPacket::decode_request(&mut source) { Ok(request) => { info!("received request: {:?}", request); From 5d007201ca35ddef7289afbbb137a490c42419eb Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 25 Sep 2025 13:41:07 +1000 Subject: [PATCH 110/393] Handing consecutive request in a buffer with some problems The issue that is left to fix is what happen when the second request is trunked and cannot be decoded. I am contemplating creating a "truncated" request buffer that would be used when the request cannot be decoded because requires extra data to be received in the next process call. Apart from this, the current code is able to process fragmented and consecutive SSH_FXP_WRITE for files with occasional problems caused by the issue detailed. --- demo/sftp/std/testing/test_write_requests.sh | 5 +- sftp/src/opaquefilehandle.rs | 1 + sftp/src/sftphandle.rs | 227 ++++++++++--------- sftp/src/sftpsource.rs | 98 ++++---- 4 files changed, 165 insertions(+), 166 deletions(-) diff --git a/demo/sftp/std/testing/test_write_requests.sh b/demo/sftp/std/testing/test_write_requests.sh index dc615c1a..8ac5d5e7 100755 --- a/demo/sftp/std/testing/test_write_requests.sh +++ b/demo/sftp/std/testing/test_write_requests.sh @@ -5,7 +5,7 @@ REMOTE_HOST="192.168.69.2" REMOTE_USER="any" # Define test files -FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random") +FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random" "256kB_random" "1MB_random" "2MB_random") # Generate random data files echo "Generating random data files..." @@ -13,6 +13,9 @@ dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null dd if=/dev/random bs=1024 count=65 of=./65kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=256 of=./256kB_random 2>/dev/null +dd if=/dev/random bs=1048576 count=1 of=./1MB_random 2>/dev/null +dd if=/dev/random bs=1048576 count=2 of=./2MB_random 2>/dev/null echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." diff --git a/sftp/src/opaquefilehandle.rs b/sftp/src/opaquefilehandle.rs index 2bdf739b..d96114a6 100644 --- a/sftp/src/opaquefilehandle.rs +++ b/sftp/src/opaquefilehandle.rs @@ -3,6 +3,7 @@ use crate::FileHandle; use sunset::sshwire::WireResult; /// This is the trait with the required methods for interoperability between different opaque file handles +/// used in SFTP transactions pub trait OpaqueFileHandle: Sized + Clone + core::hash::Hash + PartialEq + Eq + core::fmt::Debug { diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 55ffccc0..fdd07015 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,13 +1,13 @@ use crate::OpaqueFileHandle; use crate::proto::{ self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, - SftpPacket, Status, StatusCode, Write, + SftpPacket, Status, StatusCode, }; use crate::sftpserver::SftpServer; use crate::sftpsink::SftpSink; use crate::sftpsource::SftpSource; -use sunset::sshwire::{WireError, WireResult}; +use sunset::sshwire::{SSHSource, WireError, WireResult}; use core::u32; #[allow(unused_imports)] @@ -43,7 +43,9 @@ impl PartialWriteRequestTracker { } } -//#[derive(Debug, Clone)] +/// Process the raw buffers in and out from a subsystem channel decoding request and encoding responses +/// +/// It will delegate request to an `SftpServer` implemented by the library user taking into account the local system details. pub struct SftpHandler<'a, T, S> where T: OpaqueFileHandle, @@ -80,39 +82,37 @@ where buffer_out: &mut [u8], ) -> WireResult { let in_len = buffer_in.len(); - debug!("Received {:} bytes to process", in_len); + let mut buffer_in_remaining_index = 0; + + let out_len = buffer_out.len(); + let mut used_out_accumulated_index = 0; + + trace!("Received {:} bytes to process", in_len); if self.partial_write_request_tracker.is_none() & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) { return Err(WireError::PacketWrong); } - let mut source = SftpSource::new(buffer_in); - trace!("Source content: {:?}", source); + while buffer_in_remaining_index < in_len { + let mut source = + SftpSource::new(&buffer_in[buffer_in_remaining_index..]); + trace!("Source content: {:?}", source); - let mut sink = SftpSink::new(buffer_out); + let mut sink = + SftpSink::new(&mut buffer_out[used_out_accumulated_index..]); - if let Some(mut write_tracker) = self.partial_write_request_tracker.take() { - debug!( - "Processing successive chunks of a long write packet. Stored data: {:?}", - write_tracker - ); - let opaque_handle = write_tracker.get_opaque_file_handle(); + if let Some(mut write_tracker) = + self.partial_write_request_tracker.take() + { + trace!( + "Processing successive chunks of a long write packet. Stored data: {:?}", + write_tracker + ); + let opaque_handle = write_tracker.get_opaque_file_handle(); - let partial_write = { let usable_data = in_len.min(write_tracker.remain_data_len as usize); - if in_len > write_tracker.remain_data_len as usize { - // This is because we are receiving one packet and the beginning of the next one - // let sftp_num = source.peak_packet_type_with_offset( - // write_tracker.remain_data_len as usize, - // )?; - // error!( - // "There is too much data in the buffer! {:?} > than expected {:?}. There is a trailing packet in buffer: {:?}", - // in_len, write_tracker.remain_data_len, sftp_num - // ); - }; - let data_segment = source.dec_as_binstring(usable_data)?; // TODO: Do proper casting with checks u32::try_from(data_in_buffer.0.len()) @@ -122,14 +122,14 @@ where write_tracker.remain_data_offset += data_segment_len as u64; write_tracker.remain_data_len -= data_segment_len; - // let opaque_handle = write_tracker.get_file_handle(); - debug!( + trace!( "Processing successive chunks of a long write packet. Writing : opaque_handle = {:?}, write_offset = {:?}, data_segment = {:?}, data remaining = {:?}", opaque_handle, current_write_offset, data_segment, write_tracker.remain_data_len ); + match self.file_server.write( &opaque_handle, current_write_offset, @@ -141,7 +141,6 @@ where } else { push_ok(write_tracker.req_id, &mut sink)?; info!("Finished multi part Write Request"); - self.partial_write_request_tracker = None; // redundant } } Err(e) => { @@ -154,87 +153,105 @@ where )?; } }; - }; - } else { - match SftpPacket::decode_request(&mut source) { - Ok(request) => { - info!("received request: {:?}", request); - self.process_known_request(&mut sink, request).await?; - } - Err(e) => { - match e { - WireError::RanOut => { - warn!( - "RanOut for the SFTP Packet in the source buffer: {:?}", - e - ); - // let packet_total_length = source.peak_packet_len()?; - let packet_type = source.peak_packet_type()?; - match packet_type { - SftpNum::SSH_FXP_WRITE => { - let ( - file_handle, - req_id, - offset, - data_in_buffer, - write_tracker, - ) = source - .get_packet_partial_write_content_and_tracker( - )?; - debug!( - "Packet is too long for the source buffer, will write what we have now and continue writing later" - ); - trace!( - "handle = {:?}, req_id = {:?}, offset = {:?}, data_in_buffer = {:?}, write_tracker = {:?}", - file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle - req_id, - offset, - data_in_buffer, - write_tracker - ); - - match self.file_server.write( - &file_handle, - offset, - data_in_buffer.as_ref(), - ) { - Ok(_) => { - self.partial_write_request_tracker = - Some(write_tracker); - } - Err(e) => { - error!("SFTP write thrown: {:?}", e); - push_general_failure( - req_id, - "error writing ", - &mut sink, - )?; - } - }; - } - _ => { - push_general_failure( - ReqId(u32::MAX), - "Unsupported Request: Too long", - &mut sink, - )?; - } - }; - } - WireError::UnknownPacket { number: _ } => { - warn!("Error decoding SFTP Packet:{:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; - } - _ => { - error!("Error decoding SFTP Packet: {:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; + } else { + match SftpPacket::decode_request(&mut source) { + Ok(request) => { + info!("received request: {:?}", request); + self.process_known_request(&mut sink, request).await?; + } + Err(e) => { + match e { + WireError::RanOut => { + warn!( + "RanOut for the SFTP Packet in the source buffer: {:?}", + e + ); + + let packet_type = source.peak_packet_type()?; + match packet_type { + SftpNum::SSH_FXP_WRITE => { + debug!( + "about to decode packet partial write content", + ); + let ( + file_handle, + req_id, + offset, + data_in_buffer, + write_tracker, + ) = source.dec_packet_partial_write_content_and_get_tracker()?; + + trace!( + "handle = {:?}, req_id = {:?}, offset = {:?}, data_in_buffer = {:?}, write_tracker = {:?}", + file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle + req_id, + offset, + data_in_buffer, + write_tracker + ); + + match self.file_server.write( + &file_handle, + offset, + data_in_buffer.as_ref(), + ) { + Ok(_) => { + self.partial_write_request_tracker = + Some(write_tracker); + } + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure( + req_id, + "error writing ", + &mut sink, + )?; + } + }; + } + _ => { + push_general_failure( + ReqId(u32::MAX), + "Unsupported Request: Too long", + &mut sink, + )?; + } + }; + } + WireError::UnknownPacket { number: _ } => { + warn!("Error decoding SFTP Packet:{:?}", e); + push_unsupported(ReqId(u32::MAX), &mut sink)?; + } + _ => { + error!("Error decoding SFTP Packet: {:?}", e); + push_unsupported(ReqId(u32::MAX), &mut sink)?; + } } } } + }; + + // We will use these indexes to create new source and sink to process extra requests in the buffer. + buffer_in_remaining_index = in_len - source.remaining(); + if source.remaining() > 0 { + debug!( + "After processing request: Source bytes remaining = {:?}, buffer_in len = {:?} => buffer_in_remaining_index = {:?}", + source.remaining(), + in_len, + buffer_in_remaining_index + ); + trace!("Source dump: {:?}", source); + debug!( + "Buffer in left to process: {:?}", + &buffer_in[buffer_in_remaining_index..] + ); } - }; - Ok(sink.finalize()) + // TODO: What about buffer_out overflow condition? + used_out_accumulated_index += sink.finalize(); + } + + Ok(used_out_accumulated_index) } async fn process_known_request( @@ -329,7 +346,7 @@ fn push_ok(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireError> { lang: "EN".into(), }, ); - debug!("Pushing an OK status message: {:?}", response); + trace!("Pushing an OK status message: {:?}", response); response.encode_response(sink)?; Ok(()) } diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 9c97cc85..7a8a4be2 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -60,78 +60,56 @@ impl<'de> SftpSource<'de> { } } - /// Peaks the buffer for packet type adding an offset. This does not advance the reading index - /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail - /// - /// **Warning**: This might only work in special conditions, such as those where the , in other case the result will contain garbage - pub(crate) fn peak_packet_type_with_offset( - &self, - starting_offset: usize, - ) -> WireResult { - // const SFTP_ID_BUFFER_INDEX: usize = 4; // All SFTP packet have the packet type after a u32 length field - // const SFTP_MINIMUM_LENGTH: usize = 9; // Corresponds to a minimal SSH_FXP_INIT packet - if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - Err(WireError::PacketWrong) - } else { - Ok(SftpNum::from(self.buffer[starting_offset + SFTP_FIELD_ID_INDEX])) - } - } - - /// Assuming that the buffer contains a Write request packet initial bytes, Peaks the buffer for the handle length. This does not advance the reading index - /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// Assuming that the buffer contains a Write request packet initial bytes and not its totality, + /// extracts a partial version of the write request and a Write request tracker to handle and a + /// tracker to continue processing subsequent portions of the request from a SftpSource /// /// **Warning**: will only work in well formed write packets, in other case the result will contain garbage - pub(crate) fn get_packet_partial_write_content_and_tracker< + pub(crate) fn dec_packet_partial_write_content_and_get_tracker< T: OpaqueFileHandle, >( &mut self, ) -> WireResult<(T, ReqId, u64, BinString<'de>, PartialWriteRequestTracker)> { if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - Err(WireError::PacketWrong) - } else { - let prev_index = self.index; - self.index = SFTP_WRITE_REQID_INDEX; - let req_id = ReqId::dec(self)?; - let file_handle = FileHandle::dec(self)?; - - let obscured_file_handle = OpaqueFileHandle::try_from(&file_handle)?; - let offset = u64::dec(self)?; - let data_len = u32::dec(self)?; - - let data_len_in_buffer = self.buffer.len() - self.index; - let data_in_buffer = BinString(self.take(data_len_in_buffer)?); - - self.index = prev_index; - - let remain_data_len = data_len - data_len_in_buffer as u32; - let remain_data_offset = offset + data_len_in_buffer as u64; - trace!( - "Request ID = {:?}, Handle = {:?}, offset = {:?}, data length in buffer = {:?}, data in current buffer {:?} ", - req_id, file_handle, offset, data_len_in_buffer, data_in_buffer - ); - - let write_tracker = PartialWriteRequestTracker::new( - req_id, - OpaqueFileHandle::try_from(&file_handle)?, - remain_data_len, - remain_data_offset, - )?; - - Ok((obscured_file_handle, req_id, offset, data_in_buffer, write_tracker)) + return Err(WireError::PacketWrong); // TODO: Find a better error } - } - /// Used to decode the whole SSHSource as a single BinString ignoring the len field - /// - /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. - pub(crate) fn dec_all_as_binstring(&mut self) -> WireResult> { - Ok(BinString(self.take(self.buffer.len())?)) + match self.peak_packet_type()? { + SftpNum::SSH_FXP_WRITE => {} + _ => return Err(WireError::PacketWrong), // TODO: Find a better error + }; + + let prev_index = self.index; + self.index = SFTP_WRITE_REQID_INDEX; + let req_id = ReqId::dec(self)?; + let file_handle = FileHandle::dec(self)?; + + let obscured_file_handle = OpaqueFileHandle::try_from(&file_handle)?; + let offset = u64::dec(self)?; + let data_len = u32::dec(self)?; + + let data_len_in_buffer = self.buffer.len() - self.index; + let data_in_buffer = BinString(self.take(data_len_in_buffer)?); + + let remain_data_len = data_len - data_len_in_buffer as u32; + let remain_data_offset = offset + data_len_in_buffer as u64; + trace!( + "Request ID = {:?}, Handle = {:?}, offset = {:?}, data length in buffer = {:?}, data in current buffer {:?} ", + req_id, file_handle, offset, data_len_in_buffer, data_in_buffer + ); + + let write_tracker = PartialWriteRequestTracker::new( + req_id, + OpaqueFileHandle::try_from(&file_handle)?, + remain_data_len, + remain_data_offset, + )?; + + Ok((obscured_file_handle, req_id, offset, data_in_buffer, write_tracker)) } - /// Used to decode a slice of SSHSource as a single BinString ignoring the len field + /// Used to decode a slice of SSHSource as a single BinString /// /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. pub(crate) fn dec_as_binstring( From bd1a7f2650628923d243265157e207dcda155c9c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 25 Sep 2025 14:34:47 +1000 Subject: [PATCH 111/393] WIP: Modifying an error condition to RanOut and adding peak_packet_len --- sftp/src/sftpsource.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 7a8a4be2..1a6c1bb2 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -54,12 +54,30 @@ impl<'de> SftpSource<'de> { /// **Warning**: will only work in well formed packets, in other case the result will contain garbage pub(crate) fn peak_packet_type(&self) -> WireResult { if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - Err(WireError::PacketWrong) + Err(WireError::RanOut) } else { Ok(SftpNum::from(self.buffer[SFTP_FIELD_ID_INDEX])) } } + /// Peaks the buffer for packet length. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: will only work in well formed packets, in other case the result will contain garbage + pub(crate) fn peak_packet_len(&self) -> WireResult { + if self.buffer.len() < SFTP_FIELD_LEN_INDEX + SFTP_FIELD_LEN_LENGTH { + Err(WireError::RanOut) + } else { + let bytes: [u8; 4] = self.buffer + [SFTP_FIELD_LEN_INDEX..SFTP_FIELD_LEN_INDEX + SFTP_FIELD_LEN_LENGTH] + .try_into() + .expect("slice length mismatch"); + + Ok(u32::from_be_bytes(bytes)) + } + } + /// Assuming that the buffer contains a Write request packet initial bytes and not its totality, /// extracts a partial version of the write request and a Write request tracker to handle and a /// tracker to continue processing subsequent portions of the request from a SftpSource @@ -118,4 +136,8 @@ impl<'de> SftpSource<'de> { ) -> WireResult> { Ok(BinString(self.take(len)?)) } + + pub(crate) fn buffer_ref(&self) -> &[u8] { + self.buffer.clone() + } } From f07dba181b74198f973bef2f85ec2d8d9de7e96e Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 2 Oct 2025 08:15:51 +1000 Subject: [PATCH 112/393] WIP: Restructuring SftpHandle. Adding a FSM Not finished, but this structure prevents borrowing issues --- demo/sftp/std/src/main.rs | 19 +- sftp/src/lib.rs | 7 + sftp/src/requestholder.rs | 288 ++++++++++++++++++ sftp/src/sftperror.rs | 51 +++- sftp/src/sftphandle.rs | 597 +++++++++++++++++++++++++++++--------- sftp/src/sftpsource.rs | 7 +- 6 files changed, 822 insertions(+), 147 deletions(-) create mode 100644 sftp/src/requestholder.rs diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 63be11e3..b70e9046 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -1,14 +1,13 @@ use sunset::*; use sunset_async::{ProgressHolder, SSHServer, SunsetMutex, SunsetRawMutex}; -use sunset_sftp::SftpHandler; +use sunset_sftp::{RequestHolder, SftpHandler}; pub(crate) use sunset_demo_common as demo_common; use demo_common::{DemoCommon, DemoServer, SSHConfig}; use crate::{ - demoopaquefilehandle::DemoOpaqueFileHandle, - demosftpserver::{DemoSftpServer, PrivateFileHandler}, + demoopaquefilehandle::DemoOpaqueFileHandle, demosftpserver::DemoSftpServer, }; use embedded_io_async::{Read, Write}; @@ -148,8 +147,13 @@ impl DemoServer for StdDemo { info!("SFTP loop has received a channel handle {:?}", ch.num()); + // TODO: Do some research to find reasonable default buffer lengths let mut buffer_in = [0u8; 512]; - let mut buffer_out = [0u8; 512]; + let mut buffer_out = [0u8; 384]; + let mut incomplete_request_buffer = [0u8; 128]; // TODO: Find a non arbitrary length + + let mut incomplete_request_holder = + RequestHolder::new(&mut incomplete_request_buffer); match { let mut stdio = serv.stdio(ch).await?; @@ -160,6 +164,7 @@ impl DemoServer for StdDemo { let mut sftp_handler = SftpHandler::::new( &mut file_server, + // &mut incomplete_request_buffer, ); loop { let lr = stdio.read(&mut buffer_in).await?; @@ -170,7 +175,11 @@ impl DemoServer for StdDemo { } let lw = sftp_handler - .process(&buffer_in[0..lr], &mut buffer_out) + .process( + &buffer_in[0..lr], + &mut incomplete_request_holder, + &mut buffer_out, + ) .await?; if lw > 0 { stdio.write(&mut buffer_out[0..lw]).await?; diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 0b9278a1..ee2019f4 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,19 +1,26 @@ mod opaquefilehandle; mod proto; +mod requestholder; mod sftphandle; mod sftpserver; mod sftpsink; mod sftpsource; +mod sftperror; + pub use sftpserver::DirReply; pub use sftpserver::ReadReply; pub use sftpserver::SftpOpResult; pub use sftpserver::SftpServer; +pub use requestholder::RequestHolder; + pub use sftphandle::SftpHandler; pub use opaquefilehandle::{OpaqueFileHandle, OpaqueFileHandleManager, PathFinder}; +pub use sftperror::{SftpError, SftpResult}; + pub use proto::Attrs; pub use proto::FileHandle; pub use proto::Filename; diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs new file mode 100644 index 00000000..7b6df9b7 --- /dev/null +++ b/sftp/src/requestholder.rs @@ -0,0 +1,288 @@ +use crate::{proto, sftperror, sftpsource::SftpSource}; + +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; +use sunset::sshwire::WireError; + +#[derive(Debug)] +pub(crate) enum RequestHolderError { + /// The slice to hold is too long + NoRoom, + /// The slice holder is keeping a slice already. Consideer cleaning + Busy, + /// The slice holder is empty + Empty, + /// The instance has been invalidated + Invalid, + /// There is not enough data in the slice we are trying to add. we need more data + RanOut, + /// WireError + WireError(WireError), +} + +impl From for RequestHolderError { + fn from(value: WireError) -> Self { + RequestHolderError::WireError(value) + } +} + +pub(crate) type RequestHolderResult = Result; + +/// Helper struct to manage short fragmented requests that have been +/// received in consecutive read operations +/// +/// For requests exceeding the length of buffers other techniques, such +/// as composing them into multiple request, might help reducing the +/// required buffer sizes. This is recommended for restricted environments. +/// +/// The intended use for this RequestHolder is (in order): +/// - `new`: Initialize the struct with a slice that will keep the +/// request in memory +/// +/// - `try_hold`: load the data for an incomplete request +/// +/// - `try_append_for_valid_request`: append more data from another +/// slice to complete the request +/// +/// - `try_get_ref`: returns a reference to the portion of the slice +/// containing a request +/// +/// - `reset`: reset counters and flags to allow `try_hold` a new request +/// +/// - **OR** `invalidate`: return the reference to the slice provided in `new` +/// and mark the structure as invalid. At this point it should be disposed +#[derive(Debug)] +pub struct RequestHolder<'a> { + /// The buffer used to contain the data for the request + buffer: &'a mut [u8], + /// The index of the last byte in the buffer containing usable data + buffer_fill_index: usize, + /// Number of bytes appended to foundational `try_hold` slice + appended: usize, + /// Marks the structure as invalid + invalid: bool, + /// Used to mark when the structure is holding data + busy: bool, +} + +impl<'a> RequestHolder<'a> { + /// The buffer will be used to hold a full request. Choose a + /// reasonable size for this buffer. + pub fn new(buffer: &'a mut [u8]) -> Self { + RequestHolder { + buffer: buffer, + buffer_fill_index: 0, + invalid: false, + busy: false, + appended: 0, + } + } + + /// Resets the structure allowing it to hold a new request. + /// + /// Will not remove the previous data from the buffer + pub fn reset(&mut self) -> () { + self.busy = false; + self.invalid = false; + self.buffer_fill_index = 0; + self.appended = 0; + } + + /// Invalidates the current instance and returns its original buffer. Does not erase previous data + pub fn invalidate(&mut self) -> RequestHolderResult<&[u8]> { + if !self.busy { + return Err(RequestHolderError::Empty); + } + if self.invalid { + return Err(RequestHolderError::Invalid); + } + + self.invalid = true; + // self.buffer_fill_index = 0; + Ok(&self.buffer) + } + + /// Uses the internal buffer to store a copy of the provided slice + /// + /// The definition of `try_hold` and `try_append_slice` separately + /// is deliberated to follow an order in composing the held request + /// + /// returns the number of bytes read from the slice + pub fn try_hold(&mut self, slice: &[u8]) -> RequestHolderResult { + if self.busy { + return Err(RequestHolderError::Busy); + } + if self.invalid { + return Err(RequestHolderError::Invalid); + } + + self.busy = true; + self.try_append_slice(slice)?; + let read_in = self.appended(); + self.appended = 0; + Ok(read_in) + } + + /// Appends a slice to the internal buffer. Requires the buffer to + /// be busy by using `try_hold` first + /// + /// Increases the `appended` counter + /// + /// Returns the number of bytes appended + fn try_append_slice(&mut self, slice: &[u8]) -> RequestHolderResult<()> { + if slice.len() == 0 { + warn!("try appending a zero length slice"); + return Ok(()); + } + if !self.busy { + return Err(RequestHolderError::Empty); + } + + if self.invalid { + return Err(RequestHolderError::Invalid); + } + + let in_len = slice.len(); + if in_len > self.remaining_len() { + return Err(RequestHolderError::NoRoom); + } + debug!("Adding: {:?}", slice); + + self.buffer[self.buffer_fill_index..self.buffer_fill_index + in_len] + .copy_from_slice(slice); + + self.buffer_fill_index += in_len; + debug!( + "RequestHolder: index = {:?}, slice = {:?}", + self.buffer_fill_index, + self.try_get_ref()? + ); + self.appended += in_len; + Ok(()) + } + + /// Using the content of the `RequestHolder` tries to find a valid + /// SFTP request appending from slice into the internal buffer to + /// form a valid request + /// + /// Returns the number of bytes appended + pub fn try_append_for_valid_request( + &mut self, + slice: &[u8], + ) -> RequestHolderResult<()> { + self.appended = 0; // reset appended bytes counter + + debug!( + "try_append_for_valid_request: self = {:?}\n\ + Space left = {:?}\n\ + Length of slice to append from = {:?}", + self, + self.remaining_len(), + slice.len() + ); + + if self.invalid { + return Err(RequestHolderError::Invalid); + } + + if !self.busy { + return Err(RequestHolderError::Empty); + } + + // This makes sure that we do not try to read more slice than we can + if self.buffer_fill_index + slice.len() < proto::SFTP_FIELD_ID_INDEX { + self.try_append_slice(&slice)?; + return Err(RequestHolderError::RanOut); + } + + let complete_to_id_index = proto::SFTP_FIELD_ID_INDEX + .checked_sub(self.buffer_fill_index - 1) + .unwrap_or(0); + + if complete_to_id_index > 0 { + warn!( + "The held fragment len = {:?}, is insufficient to peak\ + the length and type. Will append {:?} to reach the \ + id field index: {:?}", + self.buffer_fill_index, + complete_to_id_index, + proto::SFTP_FIELD_ID_INDEX + ); + if complete_to_id_index > slice.len() { + self.try_append_slice(&slice)?; + error!( + "The slice to include to the held fragment is too \ + short to complete to id index. More data is required." + ); + return Err(RequestHolderError::RanOut); + } else { + self.try_append_slice(&slice[..complete_to_id_index])?; + }; + } + + let packet_len = + SftpSource::new(self.try_get_ref()?).peak_packet_len()? as usize; + + let packet_type = SftpSource::new(self.try_get_ref()?).peak_packet_type()?; + debug!("Request len = {:?}, type = {:?}", packet_len, packet_type); + + let remaining_packet_len = packet_len - self.buffer_fill_index; // TODO: Careful, the packet len does not include the packet len field + + if remaining_packet_len <= (self.remaining_len()) { + if remaining_packet_len > slice.len() { + self.try_append_slice(&slice[self.appended()..])?; + error!( + "The slice to include to the held fragment does not \ + contain the whole packet. More data is required." + ); + return Err(RequestHolderError::RanOut); + } + self.try_append_slice(&slice[self.appended()..])?; // The only Ok + } else { + warn!( + "The request does not fit in the buffer: \ + (req len = {:?} > buffer len = {:?} )", + packet_len, + self.buffer.len() + ); + if self.remaining_len() < (slice.len() - self.appended()) { + self.try_append_slice( + &slice[self.appended()..self.remaining_len()], + )?; + } else { + self.try_append_slice(&slice[self.appended()..])?; + } + return Err(RequestHolderError::NoRoom); + } + Ok(()) + } + + /// Gets a reference to the slice that it is holding + pub fn try_get_ref(&self) -> RequestHolderResult<&[u8]> { + if self.invalid { + return Err(RequestHolderError::Invalid); + } + + if self.busy { + Ok(&self.buffer[..self.buffer_fill_index]) + } else { + Err(RequestHolderError::Empty) + } + } + + /// Returns true if it has a slice in its buffer + pub fn is_busy(&self) -> bool { + self.busy + } + + /// Returns the bytes appened in the last call to `try_append_for_valid_request` + pub fn appended(&self) -> usize { + self.appended + } + + /// Returns the number of bytes unused at the end of the buffer, + /// this is, the remaining length + fn remaining_len(&self) -> usize { + self.buffer.len() - self.buffer_fill_index - 1 // Off by one? + } +} diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index b78393bb..98192a4d 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -1,11 +1,19 @@ use core::convert::From; +use sunset::Error as SunsetError; use sunset::sshwire::WireError; +use crate::{SftpOpResult, StatusCode, requestholder::RequestHolderError}; + #[derive(Debug)] pub enum SftpError { + NotInitialized, + AlreadyInitialized, + MalformedPacket, WireError(WireError), - // SshError(SshError), + OperationError(StatusCode), + SunsetError(SunsetError), + RequestHolderError(RequestHolderError), } impl From for SftpError { @@ -14,4 +22,45 @@ impl From for SftpError { } } +impl From for SftpError { + fn from(value: SunsetError) -> Self { + SftpError::SunsetError(value) + } +} + +impl From for SftpError { + fn from(value: StatusCode) -> Self { + SftpError::OperationError(value) + } +} + +impl From for SftpError { + fn from(value: RequestHolderError) -> Self { + SftpError::RequestHolderError(value) + } +} + +impl From for WireError { + fn from(value: SftpError) -> Self { + match value { + SftpError::WireError(wire_error) => wire_error, + _ => WireError::PacketWrong, + } + } +} + +impl From for SunsetError { + fn from(value: SftpError) -> Self { + match value { + SftpError::SunsetError(error) => error, + SftpError::WireError(wire_error) => wire_error.into(), + SftpError::NotInitialized => SunsetError::PacketWrong {}, + SftpError::AlreadyInitialized => SunsetError::PacketWrong {}, + SftpError::MalformedPacket => SunsetError::PacketWrong {}, + SftpError::OperationError(_) => SunsetError::PacketWrong {}, + SftpError::RequestHolderError(request_holder_error) => SunsetError::Bug, + } + } +} + pub type SftpResult = Result; diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index fdd07015..c9b8fa20 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,18 +1,40 @@ -use crate::OpaqueFileHandle; use crate::proto::{ - self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, - SftpPacket, Status, StatusCode, + self, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, SFTP_MINIMUM_PACKET_LEN, + SFTP_VERSION, SftpNum, SftpPacket, Status, StatusCode, }; +use crate::requestholder::{RequestHolder, RequestHolderError}; +use crate::sftperror::SftpResult; use crate::sftpserver::SftpServer; use crate::sftpsink::SftpSink; use crate::sftpsource::SftpSource; +use crate::{OpaqueFileHandle, SftpError}; +use sunset::Error as SunsetError; +use sunset::Error; use sunset::sshwire::{SSHSource, WireError, WireResult}; -use core::u32; +use core::{u32, usize}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -use std::usize; + +#[derive(Default, Debug, PartialEq, Eq)] +enum SftpHandleState { + /// The handle is not initialized + #[default] + Initializing, + /// The handle is ready to process requests + Idle, + /// The request is fragmented and needs special handling + Fragmented(FragmentedRequestState), +} + +#[derive(Debug, PartialEq, Eq)] +enum FragmentedRequestState { + /// A request that is clipped needs to be process. It cannot be decoded and more bytes are needed + ProcessingClippedRequest, + /// A request, with a length over the incoming buffer capacity is being processed + ProcessingLongRequest, +} /// Used to keep record of a long SFTP Write request that does not fit in receiving buffer and requires processing in batches #[derive(Debug)] @@ -51,6 +73,9 @@ where T: OpaqueFileHandle, S: SftpServer<'a, T>, { + /// Holds the internal state if the SFTP handle + state: SftpHandleState, + /// The local SFTP File server implementing the basic SFTP requests defined by `SftpServer` file_server: &'a mut S, @@ -71,202 +96,499 @@ where file_server, initialized: false, partial_write_request_tracker: None, + state: SftpHandleState::default(), } } /// Decodes the buffer_in request, process the request delegating operations to an Struct implementing SftpServer, - /// serializes an answer in buffer_out and return the length used in buffer_out + /// serializes an answer in buffer_out and **returns** the length used in buffer_out pub async fn process( &mut self, buffer_in: &[u8], + incomplete_request_holder: &mut RequestHolder<'_>, buffer_out: &mut [u8], - ) -> WireResult { + ) -> SftpResult { let in_len = buffer_in.len(); let mut buffer_in_remaining_index = 0; - let out_len = buffer_out.len(); let mut used_out_accumulated_index = 0; trace!("Received {:} bytes to process", in_len); - if self.partial_write_request_tracker.is_none() + + // let mut pending_incomplete_request = incomplete_request_holder.is_busy(); + // let pending_long_request = self.partial_write_request_tracker.is_some(); + + if !matches!(self.state, SftpHandleState::Fragmented(_)) & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) { - return Err(WireError::PacketWrong); + return Err(WireError::PacketWrong.into()); } while buffer_in_remaining_index < in_len { - let mut source = - SftpSource::new(&buffer_in[buffer_in_remaining_index..]); - trace!("Source content: {:?}", source); - let mut sink = SftpSink::new(&mut buffer_out[used_out_accumulated_index..]); - if let Some(mut write_tracker) = - self.partial_write_request_tracker.take() - { - trace!( - "Processing successive chunks of a long write packet. Stored data: {:?}", - write_tracker - ); - let opaque_handle = write_tracker.get_opaque_file_handle(); + debug!("SFTP Process State: {:?}", self.state); - let usable_data = in_len.min(write_tracker.remain_data_len as usize); + match &self.state { + SftpHandleState::Fragmented(fragment_case) => { + match fragment_case { + FragmentedRequestState::ProcessingClippedRequest => { + let append_result = incomplete_request_holder + .try_append_for_valid_request( + &buffer_in[buffer_in_remaining_index..], + ); - let data_segment = source.dec_as_binstring(usable_data)?; + if let Err(e) = append_result { + match e { + RequestHolderError::RanOut => { + warn!( + "There was not enough bytes in the buffer_in. We will continue adding bytes" + ); + buffer_in_remaining_index += + incomplete_request_holder.appended(); + continue; + } + RequestHolderError::NoRoom => { + todo!( + "This is an incomplete long packet situation. You hope that it is a Write request" + ) + } + _ => { + error!( + "Unhandled error completing incomplete request {:?}", + e, + ); + return Err(SunsetError::Bug.into()); + } + } + } + let mut source = SftpSource::new( + &incomplete_request_holder.try_get_ref()?, + ); + trace!("Internal Source Content: {:?}", source); - // TODO: Do proper casting with checks u32::try_from(data_in_buffer.0.len()) - let data_segment_len = data_segment.0.len() as u32; + let sftp_packet = + SftpPacket::decode_request(&mut source); - let current_write_offset = write_tracker.remain_data_offset; - write_tracker.remain_data_offset += data_segment_len as u64; - write_tracker.remain_data_len -= data_segment_len; + // TODO: Here we have to check the append_result or simply the sftp_packet to handle Errors such as TooLongRanOut or similar - trace!( - "Processing successive chunks of a long write packet. Writing : opaque_handle = {:?}, write_offset = {:?}, data_segment = {:?}, data remaining = {:?}", - opaque_handle, - current_write_offset, - data_segment, - write_tracker.remain_data_len - ); + let used = incomplete_request_holder.appended(); + buffer_in_remaining_index += used; - match self.file_server.write( - &opaque_handle, - current_write_offset, - data_segment.as_ref(), - ) { - Ok(_) => { - if write_tracker.remain_data_len > 0 { - self.partial_write_request_tracker = Some(write_tracker); - } else { - push_ok(write_tracker.req_id, &mut sink)?; - info!("Finished multi part Write Request"); + incomplete_request_holder.reset(); + + todo!("FragmentedRequestState::ProcessingClippedRequest") + } + FragmentedRequestState::ProcessingLongRequest => { + let mut source = SftpSource::new( + &buffer_in[buffer_in_remaining_index..], + ); + trace!("Source content: {:?}", source); + + let mut write_tracker = if let Some(wt) = + self.partial_write_request_tracker.take() + { + wt + } else { + return Err(SftpError::SunsetError( + SunsetError::Bug, + )); + }; + + let opaque_handle = + write_tracker.get_opaque_file_handle(); + + let usable_data = + in_len.min(write_tracker.remain_data_len as usize); + + let data_segment = // Fails!! + source.dec_as_binstring(usable_data)?; + + let data_segment_len = + u32::try_from(data_segment.0.len()) + .map_err(|e| SunsetError::Bug)?; + let current_write_offset = + write_tracker.remain_data_offset; + write_tracker.remain_data_offset += + data_segment_len as u64; + write_tracker.remain_data_len -= data_segment_len; + + debug!( + "Processing successive chunks of a long write packet. \ + Writing : opaque_handle = {:?}, write_offset = {:?}, \ + data_segment = {:?}, data remaining = {:?}", + opaque_handle, + current_write_offset, + data_segment, + write_tracker.remain_data_len + ); + + match self.file_server.write( + &opaque_handle, + current_write_offset, + data_segment.as_ref(), + ) { + Ok(_) => { + if write_tracker.remain_data_len > 0 { + self.partial_write_request_tracker = + Some(write_tracker); + } else { + push_ok(write_tracker.req_id, &mut sink)?; + info!("Finished multi part Write Request"); + self.state = SftpHandleState::Idle; + } + } + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure( + write_tracker.req_id, + "error writing", + &mut sink, + )?; + self.state = SftpHandleState::Idle; + } + }; + buffer_in_remaining_index = in_len - source.remaining(); } } - Err(e) => { - self.partial_write_request_tracker = None; - error!("SFTP write thrown: {:?}", e); - push_general_failure( - write_tracker.req_id, - "error writing", - &mut sink, - )?; - } - }; - } else { - match SftpPacket::decode_request(&mut source) { - Ok(request) => { - info!("received request: {:?}", request); - self.process_known_request(&mut sink, request).await?; - } - Err(e) => { - match e { - WireError::RanOut => { - warn!( - "RanOut for the SFTP Packet in the source buffer: {:?}", - e - ); + } - let packet_type = source.peak_packet_type()?; - match packet_type { - SftpNum::SSH_FXP_WRITE => { - debug!( - "about to decode packet partial write content", - ); - let ( - file_handle, - req_id, - offset, - data_in_buffer, - write_tracker, - ) = source.dec_packet_partial_write_content_and_get_tracker()?; - - trace!( - "handle = {:?}, req_id = {:?}, offset = {:?}, data_in_buffer = {:?}, write_tracker = {:?}", - file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle - req_id, - offset, - data_in_buffer, - write_tracker + _ => { + let mut source = + SftpSource::new(&buffer_in[buffer_in_remaining_index..]); + trace!("Source content: {:?}", source); + + let sftp_packet = SftpPacket::decode_request(&mut source); + + match self.state { + SftpHandleState::Fragmented(_) => { + return Err( + SftpError::SunsetError(SunsetError::Bug).into() + ); + } + SftpHandleState::Initializing => match sftp_packet { + Ok(request) => { + match request { + SftpPacket::Init(init_version_client) => { + let version = + SftpPacket::Version(InitVersionLowest { + version: SFTP_VERSION, + }); + + info!("Sending '{:?}'", version); + version.encode_response(&mut sink)?; + self.state = SftpHandleState::Idle; + } + _ => { + error!( + "Request received before init: {:?}", + request ); + return Err(SftpError::NotInitialized); + } + }; + } + Err(_) => { + error!( + "Malformed SFTP Packet before Init: {:?}", + sftp_packet + ); + return Err(SftpError::MalformedPacket); + } + }, + SftpHandleState::Idle => { + match sftp_packet { + Ok(request) => match request { + SftpPacket::Init(init_version_client) => { + return Err(SftpError::MalformedPacket); + } + SftpPacket::PathInfo(req_id, path_info) => { + let a_name = self + .file_server + .realpath(path_info.path.as_str()?)?; - match self.file_server.write( - &file_handle, - offset, - data_in_buffer.as_ref(), + let response = + SftpPacket::Name(req_id, a_name); + + response.encode_response(&mut sink)?; + } + SftpPacket::Open(req_id, open) => { + match self.file_server.open( + open.filename.as_str()?, + &open.attrs, ) { - Ok(_) => { - self.partial_write_request_tracker = - Some(write_tracker); + Ok(opaque_file_handle) => { + let response = SftpPacket::Handle( + req_id, + proto::Handle { + handle: opaque_file_handle + .into_file_handle(), + }, + ); + response + .encode_response(&mut sink)?; + info!("Sending '{:?}'", response); } + Err(status_code) => { + error!( + "Open failed: {:?}", + status_code + ); + push_general_failure( + req_id, "", &mut sink, + )?; + } + }; + } + SftpPacket::Write(req_id, write) => { + match self.file_server.write( + &T::try_from(&write.handle)?, + write.offset, + write.data.as_ref(), + ) { + Ok(_) => push_ok(req_id, &mut sink)?, Err(e) => { error!("SFTP write thrown: {:?}", e); push_general_failure( req_id, - "error writing ", + "error writing", &mut sink, - )?; + )? } }; } + SftpPacket::Close(req_id, close) => { + match self + .file_server + .close(&T::try_from(&close.handle)?) + { + Ok(_) => push_ok(req_id, &mut sink)?, + Err(e) => { + error!("SFTP Close thrown: {:?}", e); + push_general_failure( + req_id, "", &mut sink, + )? + } + } + } _ => { - push_general_failure( + error!("Unsuported request type"); + push_unsupported(ReqId(0), &mut sink)?; + } + }, + Err(e) => match e { + WireError::RanOut => { + warn!( + "RanOut for the SFTP Packet in the source buffer: {:?}", + e + ); + + match self.process_ran_out(&mut sink, &mut source) { + Ok(_) => { + self.state = + SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) + } + Err(e) => match e { + SftpError::WireError(WireError::RanOut) => { + let read = incomplete_request_holder + .try_hold( + &buffer_in + [buffer_in_remaining_index..], + )?; // Fails because it does not fit. Also. It is not the beginning of a new packet + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest) + } + _ => return (Err(SunsetError::Bug.into())), + }, + }; + } + WireError::UnknownPacket { number: _ } => { + warn!("Error decoding SFTP Packet:{:?}", e); + push_unsupported( ReqId(u32::MAX), - "Unsupported Request: Too long", &mut sink, )?; } - }; - } - WireError::UnknownPacket { number: _ } => { - warn!("Error decoding SFTP Packet:{:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; - } - _ => { - error!("Error decoding SFTP Packet: {:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; - } + _ => { + error!( + "Error decoding SFTP Packet: {:?}", + e + ); + push_unsupported( + ReqId(u32::MAX), + &mut sink, + )?; + } + }, + }; } } + buffer_in_remaining_index = in_len - source.remaining(); } }; + used_out_accumulated_index += sink.finalize(); + } - // We will use these indexes to create new source and sink to process extra requests in the buffer. - buffer_in_remaining_index = in_len - source.remaining(); - if source.remaining() > 0 { - debug!( - "After processing request: Source bytes remaining = {:?}, buffer_in len = {:?} => buffer_in_remaining_index = {:?}", - source.remaining(), - in_len, - buffer_in_remaining_index - ); - trace!("Source dump: {:?}", source); - debug!( - "Buffer in left to process: {:?}", - &buffer_in[buffer_in_remaining_index..] + Ok(used_out_accumulated_index) + } + + fn process_ran_out( + &mut self, + sink: &mut SftpSink<'_>, + source: &mut SftpSource<'_>, + ) -> Result<(), SftpError> { + let packet_type = source.peak_packet_type()?; + match packet_type { + SftpNum::SSH_FXP_WRITE => { + debug!("about to decode packet partial write content",); + let ( + obscured_file_handle, + req_id, + offset, + data_in_buffer, + write_tracker, + ) = source.dec_packet_partial_write_content_and_get_tracker()?; + + trace!( + "obscured_file_handle = {:?}, req_id = {:?}, \ + offset = {:?}, data_in_buffer = {:?}, \ + write_tracker = {:?}", + obscured_file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle + req_id, + offset, + data_in_buffer, + write_tracker, ); + + match self.file_server.write( + &obscured_file_handle, + offset, + data_in_buffer.as_ref(), + ) { + Ok(_) => { + self.partial_write_request_tracker = Some(write_tracker); + } + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure(req_id, "error writing ", sink)?; + } + }; } + _ => { + error!("Packet type could not be handled {:?}", packet_type); + push_general_failure( + ReqId(u32::MAX), + "Unsupported Request: Too long", + sink, + )?; + } + }; + Ok(()) + } - // TODO: What about buffer_out overflow condition? - used_out_accumulated_index += sink.finalize(); + async fn process_known_request( + // &self, + // &mut self, + initialized: &mut bool, + file_server: &mut S, + sink: &mut SftpSink<'_>, + request: SftpPacket<'_>, + ) -> Result<(), WireError> { + if !*initialized && !matches!(request, SftpPacket::Init(_)) { + push_general_failure(ReqId(u32::MAX), "Not Initialized", sink)?; + error!("Request sent before init: {:?}", request); + return Ok(()); } + match request { + SftpPacket::Init(_) => { + // TODO: Do a real check, provide the lowest version or return an error if the + // client cannot handle the server SFTP_VERSION + let version = + SftpPacket::Version(InitVersionLowest { version: SFTP_VERSION }); - Ok(used_out_accumulated_index) + info!("Sending '{:?}'", version); + + version.encode_response(sink)?; + + *initialized = true; + } + SftpPacket::PathInfo(req_id, path_info) => { + let a_name = + file_server + .realpath(path_info.path.as_str().expect( + "Could not deref and the errors are not harmonized", + )) + .expect("Could not deref and the errors are not harmonized"); + + let response = SftpPacket::Name(req_id, a_name); + + response.encode_response(sink)?; + } + SftpPacket::Open(req_id, open) => { + match file_server.open(open.filename.as_str()?, &open.attrs) { + Ok(opaque_file_handle) => { + let response = SftpPacket::Handle( + req_id, + proto::Handle { + handle: opaque_file_handle.into_file_handle(), + }, + ); + response.encode_response(sink)?; + info!("Sending '{:?}'", response); + } + Err(status_code) => { + error!("Open failed: {:?}", status_code); + push_general_failure(req_id, "", sink)?; + } + }; + } + SftpPacket::Write(req_id, write) => { + match file_server.write( + &T::try_from(&write.handle)?, + write.offset, + write.data.as_ref(), + ) { + Ok(_) => push_ok(req_id, sink)?, + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure(req_id, "error writing ", sink)? + } + }; + } + SftpPacket::Close(req_id, close) => { + match file_server.close(&T::try_from(&close.handle)?) { + Ok(_) => push_ok(req_id, sink)?, + Err(e) => { + error!("SFTP Close thrown: {:?}", e); + push_general_failure(req_id, "", sink)? + } + } + } + _ => { + error!("This kind of request is not supported: {:?}", request); + push_unsupported(ReqId(0), sink)?; + } + }; + Ok(()) } - async fn process_known_request( - &mut self, + async fn process_request( + // &self, + // &mut self, + initialized: &mut bool, + file_server: &mut S, sink: &mut SftpSink<'_>, request: SftpPacket<'_>, ) -> Result<(), WireError> { - if !self.initialized && !matches!(request, SftpPacket::Init(_)) { + if !*initialized && !matches!(request, SftpPacket::Init(_)) { push_general_failure(ReqId(u32::MAX), "Not Initialized", sink)?; error!("Request sent before init: {:?}", request); return Ok(()); } match request { SftpPacket::Init(_) => { - // TODO: Do a real check, provide the lowest version or return an error if the client cannot handle the server SFTP_VERSION + // TODO: Do a real check, provide the lowest version or return an error if the + // client cannot handle the server SFTP_VERSION let version = SftpPacket::Version(InitVersionLowest { version: SFTP_VERSION }); @@ -274,11 +596,11 @@ where version.encode_response(sink)?; - self.initialized = true; + *initialized = true; } SftpPacket::PathInfo(req_id, path_info) => { let a_name = - self.file_server + file_server .realpath(path_info.path.as_str().expect( "Could not deref and the errors are not harmonized", )) @@ -289,7 +611,7 @@ where response.encode_response(sink)?; } SftpPacket::Open(req_id, open) => { - match self.file_server.open(open.filename.as_str()?, &open.attrs) { + match file_server.open(open.filename.as_str()?, &open.attrs) { Ok(opaque_file_handle) => { let response = SftpPacket::Handle( req_id, @@ -307,7 +629,7 @@ where }; } SftpPacket::Write(req_id, write) => { - match self.file_server.write( + match file_server.write( &T::try_from(&write.handle)?, write.offset, write.data.as_ref(), @@ -320,7 +642,7 @@ where }; } SftpPacket::Close(req_id, close) => { - match self.file_server.close(&T::try_from(&close.handle)?) { + match file_server.close(&T::try_from(&close.handle)?) { Ok(_) => push_ok(req_id, sink)?, Err(e) => { error!("SFTP Close thrown: {:?}", e); @@ -329,6 +651,7 @@ where } } _ => { + error!("This kind of request is not supported: {:?}", request); push_unsupported(ReqId(0), sink)?; } }; diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 1a6c1bb2..2760155d 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -1,6 +1,6 @@ use crate::proto::{ - ReqId, SFTP_FIELD_ID_INDEX, SFTP_MINIMUM_PACKET_LEN, SFTP_WRITE_REQID_INDEX, - SftpNum, + ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, + SFTP_MINIMUM_PACKET_LEN, SFTP_WRITE_REQID_INDEX, SftpNum, }; use crate::sftphandle::PartialWriteRequestTracker; use crate::{FileHandle, OpaqueFileHandle}; @@ -98,12 +98,10 @@ impl<'de> SftpSource<'de> { _ => return Err(WireError::PacketWrong), // TODO: Find a better error }; - let prev_index = self.index; self.index = SFTP_WRITE_REQID_INDEX; let req_id = ReqId::dec(self)?; let file_handle = FileHandle::dec(self)?; - let obscured_file_handle = OpaqueFileHandle::try_from(&file_handle)?; let offset = u64::dec(self)?; let data_len = u32::dec(self)?; @@ -124,6 +122,7 @@ impl<'de> SftpSource<'de> { remain_data_offset, )?; + let obscured_file_handle = OpaqueFileHandle::try_from(&file_handle)?; Ok((obscured_file_handle, req_id, offset, data_in_buffer, write_tracker)) } From 989aae0dfca10f77b2630dd27dcacc4f9c172ff3 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 2 Oct 2025 14:58:59 +1000 Subject: [PATCH 113/393] WIP: handling ok request in FragmentedRequestState::ProcessingClippedRequest This does not cover what happens with NoRoom requests such as Write request --- sftp/src/sftphandle.rs | 189 +++++++++++++++++++++++------------------ 1 file changed, 106 insertions(+), 83 deletions(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index c9b8fa20..2deb9d55 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -143,15 +143,25 @@ where match e { RequestHolderError::RanOut => { warn!( - "There was not enough bytes in the buffer_in. We will continue adding bytes" + "There was not enough bytes in the buffer_in. \ + We will continue adding bytes" ); buffer_in_remaining_index += incomplete_request_holder.appended(); continue; } RequestHolderError::NoRoom => { - todo!( - "This is an incomplete long packet situation. You hope that it is a Write request" + warn!( + "There is not enough room in incomplete request holder \ + to accommodate this packet buffer." + ) + } + RequestHolderError::WireError( + WireError::RanOut, + ) => { + warn!( + "There is not enough room in incomplete request holder \ + to accommodate this packet buffer." ) } _ => { @@ -162,6 +172,10 @@ where return Err(SunsetError::Bug.into()); } } + } else { + debug!( + "Incomplete request holder completes request!" + ); } let mut source = SftpSource::new( &incomplete_request_holder.try_get_ref()?, @@ -171,13 +185,31 @@ where let sftp_packet = SftpPacket::decode_request(&mut source); - // TODO: Here we have to check the append_result or simply the sftp_packet to handle Errors such as TooLongRanOut or similar + match sftp_packet { + Ok(request) => { + self.handle_general_request(&mut sink, request)?; + } + Err(e) => match e { + WireError::NoRoom => { + todo!("The packet do not fit in the buffer") + } + WireError::RanOut => { + todo!("Not enough data to decode the packet") + } + _ => { + error!( + "Unhandled error decoding assembled packet: {:?}", + e + ); + } + }, + } let used = incomplete_request_holder.appended(); buffer_in_remaining_index += used; incomplete_request_holder.reset(); - + self.state = SftpHandleState::Idle; todo!("FragmentedRequestState::ProcessingClippedRequest") } FragmentedRequestState::ProcessingLongRequest => { @@ -299,84 +331,9 @@ where }, SftpHandleState::Idle => { match sftp_packet { - Ok(request) => match request { - SftpPacket::Init(init_version_client) => { - return Err(SftpError::MalformedPacket); - } - SftpPacket::PathInfo(req_id, path_info) => { - let a_name = self - .file_server - .realpath(path_info.path.as_str()?)?; - - let response = - SftpPacket::Name(req_id, a_name); - - response.encode_response(&mut sink)?; - } - SftpPacket::Open(req_id, open) => { - match self.file_server.open( - open.filename.as_str()?, - &open.attrs, - ) { - Ok(opaque_file_handle) => { - let response = SftpPacket::Handle( - req_id, - proto::Handle { - handle: opaque_file_handle - .into_file_handle(), - }, - ); - response - .encode_response(&mut sink)?; - info!("Sending '{:?}'", response); - } - Err(status_code) => { - error!( - "Open failed: {:?}", - status_code - ); - push_general_failure( - req_id, "", &mut sink, - )?; - } - }; - } - SftpPacket::Write(req_id, write) => { - match self.file_server.write( - &T::try_from(&write.handle)?, - write.offset, - write.data.as_ref(), - ) { - Ok(_) => push_ok(req_id, &mut sink)?, - Err(e) => { - error!("SFTP write thrown: {:?}", e); - push_general_failure( - req_id, - "error writing", - &mut sink, - )? - } - }; - } - SftpPacket::Close(req_id, close) => { - match self - .file_server - .close(&T::try_from(&close.handle)?) - { - Ok(_) => push_ok(req_id, &mut sink)?, - Err(e) => { - error!("SFTP Close thrown: {:?}", e); - push_general_failure( - req_id, "", &mut sink, - )? - } - } - } - _ => { - error!("Unsuported request type"); - push_unsupported(ReqId(0), &mut sink)?; - } - }, + Ok(request) => { + self.handle_general_request(&mut sink, request)? + } Err(e) => match e { WireError::RanOut => { warn!( @@ -432,6 +389,72 @@ where Ok(used_out_accumulated_index) } + fn handle_general_request( + &mut self, + sink: &mut SftpSink<'_>, + request: SftpPacket<'_>, + ) -> Result<(), SftpError> + where + T: OpaqueFileHandle, + { + Ok(match request { + SftpPacket::Init(init_version_client) => { + return Err(SftpError::MalformedPacket); + } + SftpPacket::PathInfo(req_id, path_info) => { + let a_name = self.file_server.realpath(path_info.path.as_str()?)?; + + let response = SftpPacket::Name(req_id, a_name); + + response.encode_response(sink)?; + } + SftpPacket::Open(req_id, open) => { + match self.file_server.open(open.filename.as_str()?, &open.attrs) { + Ok(opaque_file_handle) => { + let response = SftpPacket::Handle( + req_id, + proto::Handle { + handle: opaque_file_handle.into_file_handle(), + }, + ); + response.encode_response(sink)?; + info!("Sending '{:?}'", response); + } + Err(status_code) => { + error!("Open failed: {:?}", status_code); + push_general_failure(req_id, "", sink)?; + } + }; + } + SftpPacket::Write(req_id, write) => { + match self.file_server.write( + &T::try_from(&write.handle)?, + write.offset, + write.data.as_ref(), + ) { + Ok(_) => push_ok(req_id, sink)?, + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure(req_id, "error writing", sink)? + } + }; + } + SftpPacket::Close(req_id, close) => { + match self.file_server.close(&T::try_from(&close.handle)?) { + Ok(_) => push_ok(req_id, sink)?, + Err(e) => { + error!("SFTP Close thrown: {:?}", e); + push_general_failure(req_id, "", sink)? + } + } + } + _ => { + error!("Unsuported request type"); + push_unsupported(ReqId(0), sink)?; + } + }) + } + fn process_ran_out( &mut self, sink: &mut SftpSink<'_>, From 54f21b7b8e215687605cb01c0c50811e4d859cfd Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 2 Oct 2025 15:23:06 +1000 Subject: [PATCH 114/393] WIP: Removing dead code --- sftp/src/sftphandle.rs | 171 ----------------------------------------- 1 file changed, 171 deletions(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 2deb9d55..88e16f72 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -509,177 +509,6 @@ where Ok(()) } - async fn process_known_request( - // &self, - // &mut self, - initialized: &mut bool, - file_server: &mut S, - sink: &mut SftpSink<'_>, - request: SftpPacket<'_>, - ) -> Result<(), WireError> { - if !*initialized && !matches!(request, SftpPacket::Init(_)) { - push_general_failure(ReqId(u32::MAX), "Not Initialized", sink)?; - error!("Request sent before init: {:?}", request); - return Ok(()); - } - match request { - SftpPacket::Init(_) => { - // TODO: Do a real check, provide the lowest version or return an error if the - // client cannot handle the server SFTP_VERSION - let version = - SftpPacket::Version(InitVersionLowest { version: SFTP_VERSION }); - - info!("Sending '{:?}'", version); - - version.encode_response(sink)?; - - *initialized = true; - } - SftpPacket::PathInfo(req_id, path_info) => { - let a_name = - file_server - .realpath(path_info.path.as_str().expect( - "Could not deref and the errors are not harmonized", - )) - .expect("Could not deref and the errors are not harmonized"); - - let response = SftpPacket::Name(req_id, a_name); - - response.encode_response(sink)?; - } - SftpPacket::Open(req_id, open) => { - match file_server.open(open.filename.as_str()?, &open.attrs) { - Ok(opaque_file_handle) => { - let response = SftpPacket::Handle( - req_id, - proto::Handle { - handle: opaque_file_handle.into_file_handle(), - }, - ); - response.encode_response(sink)?; - info!("Sending '{:?}'", response); - } - Err(status_code) => { - error!("Open failed: {:?}", status_code); - push_general_failure(req_id, "", sink)?; - } - }; - } - SftpPacket::Write(req_id, write) => { - match file_server.write( - &T::try_from(&write.handle)?, - write.offset, - write.data.as_ref(), - ) { - Ok(_) => push_ok(req_id, sink)?, - Err(e) => { - error!("SFTP write thrown: {:?}", e); - push_general_failure(req_id, "error writing ", sink)? - } - }; - } - SftpPacket::Close(req_id, close) => { - match file_server.close(&T::try_from(&close.handle)?) { - Ok(_) => push_ok(req_id, sink)?, - Err(e) => { - error!("SFTP Close thrown: {:?}", e); - push_general_failure(req_id, "", sink)? - } - } - } - _ => { - error!("This kind of request is not supported: {:?}", request); - push_unsupported(ReqId(0), sink)?; - } - }; - Ok(()) - } - - async fn process_request( - // &self, - // &mut self, - initialized: &mut bool, - file_server: &mut S, - sink: &mut SftpSink<'_>, - request: SftpPacket<'_>, - ) -> Result<(), WireError> { - if !*initialized && !matches!(request, SftpPacket::Init(_)) { - push_general_failure(ReqId(u32::MAX), "Not Initialized", sink)?; - error!("Request sent before init: {:?}", request); - return Ok(()); - } - match request { - SftpPacket::Init(_) => { - // TODO: Do a real check, provide the lowest version or return an error if the - // client cannot handle the server SFTP_VERSION - let version = - SftpPacket::Version(InitVersionLowest { version: SFTP_VERSION }); - - info!("Sending '{:?}'", version); - - version.encode_response(sink)?; - - *initialized = true; - } - SftpPacket::PathInfo(req_id, path_info) => { - let a_name = - file_server - .realpath(path_info.path.as_str().expect( - "Could not deref and the errors are not harmonized", - )) - .expect("Could not deref and the errors are not harmonized"); - - let response = SftpPacket::Name(req_id, a_name); - - response.encode_response(sink)?; - } - SftpPacket::Open(req_id, open) => { - match file_server.open(open.filename.as_str()?, &open.attrs) { - Ok(opaque_file_handle) => { - let response = SftpPacket::Handle( - req_id, - proto::Handle { - handle: opaque_file_handle.into_file_handle(), - }, - ); - response.encode_response(sink)?; - info!("Sending '{:?}'", response); - } - Err(status_code) => { - error!("Open failed: {:?}", status_code); - push_general_failure(req_id, "", sink)?; - } - }; - } - SftpPacket::Write(req_id, write) => { - match file_server.write( - &T::try_from(&write.handle)?, - write.offset, - write.data.as_ref(), - ) { - Ok(_) => push_ok(req_id, sink)?, - Err(e) => { - error!("SFTP write thrown: {:?}", e); - push_general_failure(req_id, "error writing ", sink)? - } - }; - } - SftpPacket::Close(req_id, close) => { - match file_server.close(&T::try_from(&close.handle)?) { - Ok(_) => push_ok(req_id, sink)?, - Err(e) => { - error!("SFTP Close thrown: {:?}", e); - push_general_failure(req_id, "", sink)? - } - } - } - _ => { - error!("This kind of request is not supported: {:?}", request); - push_unsupported(ReqId(0), sink)?; - } - }; - Ok(()) - } } #[inline] From 23f347e353bde45b467301f37659189f7d660fe2 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 2 Oct 2025 15:32:05 +1000 Subject: [PATCH 115/393] WIP: replacing WireErrors and minor renaming --- sftp/src/requestholder.rs | 252 +++++++++++++++++++++++++------------- sftp/src/sftperror.rs | 26 +++- sftp/src/sftphandle.rs | 93 +++++++++----- sftp/src/sftpsource.rs | 11 +- 4 files changed, 256 insertions(+), 126 deletions(-) diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs index 7b6df9b7..36f37fa3 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/requestholder.rs @@ -18,6 +18,7 @@ pub(crate) enum RequestHolderError { RanOut, /// WireError WireError(WireError), + Bug, } impl From for RequestHolderError { @@ -57,7 +58,7 @@ pub struct RequestHolder<'a> { buffer: &'a mut [u8], /// The index of the last byte in the buffer containing usable data buffer_fill_index: usize, - /// Number of bytes appended to foundational `try_hold` slice + /// Number of bytes appended in a previous `try_hold` or `try_append_for_valid_request` slice appended: usize, /// Marks the structure as invalid invalid: bool, @@ -72,42 +73,26 @@ impl<'a> RequestHolder<'a> { RequestHolder { buffer: buffer, buffer_fill_index: 0, - invalid: false, + invalid: false, // TODO: Remove if invalidate() is removed busy: false, appended: 0, } } - /// Resets the structure allowing it to hold a new request. - /// - /// Will not remove the previous data from the buffer - pub fn reset(&mut self) -> () { - self.busy = false; - self.invalid = false; - self.buffer_fill_index = 0; - self.appended = 0; - } - - /// Invalidates the current instance and returns its original buffer. Does not erase previous data - pub fn invalidate(&mut self) -> RequestHolderResult<&[u8]> { - if !self.busy { - return Err(RequestHolderError::Empty); - } - if self.invalid { - return Err(RequestHolderError::Invalid); - } - - self.invalid = true; - // self.buffer_fill_index = 0; - Ok(&self.buffer) - } - /// Uses the internal buffer to store a copy of the provided slice /// /// The definition of `try_hold` and `try_append_slice` separately /// is deliberated to follow an order in composing the held request /// - /// returns the number of bytes read from the slice + /// Increases the `appended()` counter + /// + /// returns: + /// + /// - Ok(usize): the number of bytes read from the slice + /// + /// - `Err(Busy)`: If there has been a call to `try_hold` without a call to `reset` + /// + /// - `Err(Invalid)`: If the structure has been marked as invalid previously pub fn try_hold(&mut self, slice: &[u8]) -> RequestHolderResult { if self.busy { return Err(RequestHolderError::Busy); @@ -123,55 +108,56 @@ impl<'a> RequestHolder<'a> { Ok(read_in) } - /// Appends a slice to the internal buffer. Requires the buffer to - /// be busy by using `try_hold` first + /// Resets the structure allowing it to hold a new request. /// - /// Increases the `appended` counter + /// Resets the `appended()` counter. /// - /// Returns the number of bytes appended - fn try_append_slice(&mut self, slice: &[u8]) -> RequestHolderResult<()> { - if slice.len() == 0 { - warn!("try appending a zero length slice"); - return Ok(()); - } + /// Will not clear the previous data from the buffer. + pub fn reset(&mut self) -> () { + self.busy = false; + self.invalid = false; + self.buffer_fill_index = 0; + self.appended = 0; + } + + // TODO: Remove it if it is not used + /// Invalidates the current instance and returns its original buffer. Does not erase previous data + pub fn invalidate(&mut self) -> RequestHolderResult<&[u8]> { if !self.busy { return Err(RequestHolderError::Empty); } - if self.invalid { return Err(RequestHolderError::Invalid); } - let in_len = slice.len(); - if in_len > self.remaining_len() { - return Err(RequestHolderError::NoRoom); - } - debug!("Adding: {:?}", slice); - - self.buffer[self.buffer_fill_index..self.buffer_fill_index + in_len] - .copy_from_slice(slice); - - self.buffer_fill_index += in_len; - debug!( - "RequestHolder: index = {:?}, slice = {:?}", - self.buffer_fill_index, - self.try_get_ref()? - ); - self.appended += in_len; - Ok(()) + self.invalid = true; + // self.buffer_fill_index = 0; + Ok(&self.buffer) } /// Using the content of the `RequestHolder` tries to find a valid /// SFTP request appending from slice into the internal buffer to - /// form a valid request + /// form a valid request. + /// + /// Reset and increase the `appended()` counter. + /// + /// **Returns**: + /// + /// - `Ok(())`: Full valid request + /// + /// - `Err(RanOut)`: Not enough bytes in the slice to complete a valid request or fill the buffer + /// + /// - `Err(NoRoom)`: The internal buffer is full but there is not a full valid request in the buffer + /// + /// - `Err(Invalid)`: If the structure has been marked as invalid previously + /// + /// - `Err(Empty)`: If the structure has not been loaded with `try_hold` /// - /// Returns the number of bytes appended + /// - `Err(Bug)`: An unexpected condition arises pub fn try_append_for_valid_request( &mut self, slice: &[u8], ) -> RequestHolderResult<()> { - self.appended = 0; // reset appended bytes counter - debug!( "try_append_for_valid_request: self = {:?}\n\ Space left = {:?}\n\ @@ -182,21 +168,37 @@ impl<'a> RequestHolder<'a> { ); if self.invalid { + error!("Request Holder is invalid"); return Err(RequestHolderError::Invalid); } if !self.busy { + error!("Request Holder is not busy"); return Err(RequestHolderError::Empty); } - // This makes sure that we do not try to read more slice than we can + if self.is_full() { + error!("Request Holder is full"); + return Err(RequestHolderError::NoRoom); + } + + self.appended = 0; // reset appended bytes counter + + // If we will not be able to read the SFTP packet ID we clearly need more data if self.buffer_fill_index + slice.len() < proto::SFTP_FIELD_ID_INDEX { self.try_append_slice(&slice)?; + error!( + "[Buffer fill index = {:?}] + [slice.len = {:?}] = {:?} < SFTP field id index = {:?}", + self.buffer_fill_index, + slice.len(), + self.buffer_fill_index + slice.len(), + proto::SFTP_FIELD_ID_INDEX + ); return Err(RequestHolderError::RanOut); } let complete_to_id_index = proto::SFTP_FIELD_ID_INDEX - .checked_sub(self.buffer_fill_index - 1) + .checked_sub(self.buffer_fill_index) .unwrap_or(0); if complete_to_id_index > 0 { @@ -220,39 +222,66 @@ impl<'a> RequestHolder<'a> { }; } - let packet_len = - SftpSource::new(self.try_get_ref()?).peak_packet_len()? as usize; - - let packet_type = SftpSource::new(self.try_get_ref()?).peak_packet_type()?; + let (packet_len, packet_type) = { + let temp_source = SftpSource::new(self.try_get_ref()?); + let packet_len = temp_source.peak_packet_len()? as usize; + let packet_type = temp_source.peak_packet_type()?; + (packet_len, packet_type) + }; debug!("Request len = {:?}, type = {:?}", packet_len, packet_type); - let remaining_packet_len = packet_len - self.buffer_fill_index; // TODO: Careful, the packet len does not include the packet len field + let remaining_packet_len = + packet_len - (self.buffer_fill_index - proto::SFTP_FIELD_LEN_LENGTH); + // The packet len does not include the packet len field itself (4 bytes) + // https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-3 + debug!( + "[Total Packet len = {:?}] = [Packet len copied so far = {:}] \ + - [SFTP Field len length = {:?}] + [Remaining packet len = {:?}]", + packet_len, + self.buffer_fill_index, + proto::SFTP_FIELD_LEN_LENGTH, + remaining_packet_len, + ); + assert_eq!( + packet_len, + self.buffer_fill_index - proto::SFTP_FIELD_LEN_LENGTH + + remaining_packet_len + ); + // TODO: Fix the mess with the logic and the indexes to address the slice. IT IS PANICKING + if remaining_packet_len <= self.remaining_len() { + // We have all the remaining packet bytes in the slice and fits in the buffer - if remaining_packet_len <= (self.remaining_len()) { - if remaining_packet_len > slice.len() { + if (slice.len()) < (remaining_packet_len + self.appended()) { self.try_append_slice(&slice[self.appended()..])?; - error!( - "The slice to include to the held fragment does not \ - contain the whole packet. More data is required." - ); return Err(RequestHolderError::RanOut); + } else { + self.try_append_slice( + &slice[self.appended()..remaining_packet_len], + )?; + return Ok(()); } - self.try_append_slice(&slice[self.appended()..])?; // The only Ok } else { - warn!( - "The request does not fit in the buffer: \ - (req len = {:?} > buffer len = {:?} )", - packet_len, - self.buffer.len() + // the remaining packet bytes are more than we can fit in the buffer + // But they may not fit in the slice neither + + let start = self.appended(); + let end = self.remaining_len().min(slice.len() - self.appended()); + + debug!( + "Will finally take the range: [{:?}..{:?}] from the slice [0..{:?}]", + start, + end, + slice.len() ); - if self.remaining_len() < (slice.len() - self.appended()) { - self.try_append_slice( - &slice[self.appended()..self.remaining_len()], - )?; + self.try_append_slice( + &slice[self.appended() + ..self.remaining_len().min(slice.len() - self.appended())], + )?; + if self.is_full() { + return Err(RequestHolderError::NoRoom); } else { - self.try_append_slice(&slice[self.appended()..])?; + return Err(RequestHolderError::RanOut); } - return Err(RequestHolderError::NoRoom); } Ok(()) } @@ -264,12 +293,20 @@ impl<'a> RequestHolder<'a> { } if self.busy { + debug!( + "Returning reference to: {:?}", + &self.buffer[..self.buffer_fill_index] + ); Ok(&self.buffer[..self.buffer_fill_index]) } else { Err(RequestHolderError::Empty) } } + pub fn is_full(&mut self) -> bool { + self.buffer_fill_index == self.buffer.len() + } + /// Returns true if it has a slice in its buffer pub fn is_busy(&self) -> bool { self.busy @@ -280,9 +317,56 @@ impl<'a> RequestHolder<'a> { self.appended } + /// Appends a slice to the internal buffer. Requires the buffer to + /// be busy by using `try_hold` first + /// + /// Increases the `appended` counter but does not reset it + /// + /// Returns: + /// + /// - `Ok(())`: the slice was appended + /// + /// - `Err(Invalid)`: If the structure has been marked as invalid previously + /// + /// - `Err(Empty)`: If the structure has not been loaded with `try_hold` + /// + /// - `Err(NoRoom)`: The internal buffer is full but there is not a full valid request in the buffer + fn try_append_slice(&mut self, slice: &[u8]) -> RequestHolderResult<()> { + if slice.len() == 0 { + warn!("try appending a zero length slice"); + return Ok(()); + } + if !self.busy { + return Err(RequestHolderError::Empty); + } + + if self.invalid { + return Err(RequestHolderError::Invalid); + } + + let in_len = slice.len(); + if in_len > self.remaining_len() { + return Err(RequestHolderError::NoRoom); + } + debug!("Adding: {:?}", slice); + + self.buffer[self.buffer_fill_index..self.buffer_fill_index + in_len] + .copy_from_slice(slice); + + self.buffer_fill_index += in_len; + debug!( + "RequestHolder: index = {:?}, slice = {:?}", + self.buffer_fill_index, + self.try_get_ref()? + ); + self.appended += in_len; + Ok(()) + } + /// Returns the number of bytes unused at the end of the buffer, /// this is, the remaining length fn remaining_len(&self) -> usize { - self.buffer.len() - self.buffer_fill_index - 1 // Off by one? + // self.buffer.len() - self.buffer_fill_index - 1 // TODO: Off by one? + self.buffer.len() - self.buffer_fill_index } } diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index 98192a4d..a97d52fd 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -1,5 +1,6 @@ use core::convert::From; +use log::warn; use sunset::Error as SunsetError; use sunset::sshwire::WireError; @@ -10,6 +11,7 @@ pub enum SftpError { NotInitialized, AlreadyInitialized, MalformedPacket, + NotSupported, WireError(WireError), OperationError(StatusCode), SunsetError(SunsetError), @@ -54,10 +56,26 @@ impl From for SunsetError { match value { SftpError::SunsetError(error) => error, SftpError::WireError(wire_error) => wire_error.into(), - SftpError::NotInitialized => SunsetError::PacketWrong {}, - SftpError::AlreadyInitialized => SunsetError::PacketWrong {}, - SftpError::MalformedPacket => SunsetError::PacketWrong {}, - SftpError::OperationError(_) => SunsetError::PacketWrong {}, + SftpError::NotInitialized => { + warn!("Casting error loosing information: {:?}", value); + SunsetError::PacketWrong {} + } + SftpError::NotSupported => { + warn!("Casting error loosing information: {:?}", value); + SunsetError::PacketWrong {} + } + SftpError::AlreadyInitialized => { + warn!("Casting error loosing information: {:?}", value); + SunsetError::PacketWrong {} + } + SftpError::MalformedPacket => { + warn!("Casting error loosing information: {:?}", value); + SunsetError::PacketWrong {} + } + SftpError::OperationError(_) => { + warn!("Casting error loosing information: {:?}", value); + SunsetError::PacketWrong {} + } SftpError::RequestHolderError(request_holder_error) => SunsetError::Bug, } } diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 88e16f72..c625f4b9 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -115,9 +115,6 @@ where trace!("Received {:} bytes to process", in_len); - // let mut pending_incomplete_request = incomplete_request_holder.is_busy(); - // let pending_long_request = self.partial_write_request_tracker.is_some(); - if !matches!(self.state, SftpHandleState::Fragmented(_)) & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) { @@ -134,12 +131,13 @@ where SftpHandleState::Fragmented(fragment_case) => { match fragment_case { FragmentedRequestState::ProcessingClippedRequest => { - let append_result = incomplete_request_holder + let appending_result = incomplete_request_holder .try_append_for_valid_request( + // TODO: All your problems are here. Focus &buffer_in[buffer_in_remaining_index..], ); - if let Err(e) = append_result { + if let Err(e) = appending_result { match e { RequestHolderError::RanOut => { warn!( @@ -151,6 +149,7 @@ where continue; } RequestHolderError::NoRoom => { + // Fragmented Write request... warn!( "There is not enough room in incomplete request holder \ to accommodate this packet buffer." @@ -160,7 +159,7 @@ where WireError::RanOut, ) => { warn!( - "There is not enough room in incomplete request holder \ + "WireError: There is not enough room in incomplete request holder \ to accommodate this packet buffer." ) } @@ -174,43 +173,62 @@ where } } else { debug!( - "Incomplete request holder completes request!" + "Incomplete request holder completed the request!" ); } + + let used = incomplete_request_holder.appended(); + buffer_in_remaining_index += used; + let mut source = SftpSource::new( &incomplete_request_holder.try_get_ref()?, ); trace!("Internal Source Content: {:?}", source); - let sftp_packet = + let decoding_request_result = SftpPacket::decode_request(&mut source); - match sftp_packet { + match decoding_request_result { Ok(request) => { self.handle_general_request(&mut sink, request)?; + incomplete_request_holder.reset(); + self.state = SftpHandleState::Idle; } Err(e) => match e { - WireError::NoRoom => { - todo!("The packet do not fit in the buffer") - } WireError::RanOut => { - todo!("Not enough data to decode the packet") + match self + .handle_ran_out(&mut sink, &mut source) + { + Ok(_) => { + self.state = + SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); + incomplete_request_holder.reset(); + } + Err(e) => match e { + _ => { + error!( + "handle_ran_out finished with error: {:?}", + e + ); + return (Err( + SunsetError::Bug.into() + )); + } + }, + } + } + WireError::NoRoom => { + error!("Not enough space to fit the request") } _ => { error!( "Unhandled error decoding assembled packet: {:?}", e ); + return Err(WireError::PacketWrong.into()); } }, } - - let used = incomplete_request_holder.appended(); - buffer_in_remaining_index += used; - - incomplete_request_holder.reset(); - self.state = SftpHandleState::Idle; - todo!("FragmentedRequestState::ProcessingClippedRequest") } FragmentedRequestState::ProcessingLongRequest => { let mut source = SftpSource::new( @@ -223,16 +241,19 @@ where { wt } else { - return Err(SftpError::SunsetError( - SunsetError::Bug, - )); + error!( + "BUG: FragmentedRequestState::ProcessingLongRequest cannot take the write tracker" + ); + return Err(SunsetError::Bug.into()); }; let opaque_handle = write_tracker.get_opaque_file_handle(); - let usable_data = - in_len.min(write_tracker.remain_data_len as usize); + let usable_data = source + .remaining() + .min(write_tracker.remain_data_len as usize); // TODO: Where does in_len comes from? + // in_len.min(write_tracker.remain_data_len as usize); let data_segment = // Fails!! source.dec_as_binstring(usable_data)?; @@ -341,7 +362,7 @@ where e ); - match self.process_ran_out(&mut sink, &mut source) { + match self.handle_ran_out(&mut sink, &mut source) { Ok(_) => { self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) @@ -357,7 +378,7 @@ where } _ => return (Err(SunsetError::Bug.into())), }, - }; + }; } WireError::UnknownPacket { number: _ } => { warn!("Error decoding SFTP Packet:{:?}", e); @@ -455,11 +476,15 @@ where }) } - fn process_ran_out( + /// Handles long request that do not fit in the buffers and stores a tracker + /// + /// **WARNING:** Only `SSH_FXP_WRITE` has been implemented! + /// + fn handle_ran_out( &mut self, sink: &mut SftpSink<'_>, source: &mut SftpSource<'_>, - ) -> Result<(), SftpError> { + ) -> SftpResult<()> { let packet_type = source.peak_packet_type()?; match packet_type { SftpNum::SSH_FXP_WRITE => { @@ -476,7 +501,7 @@ where "obscured_file_handle = {:?}, req_id = {:?}, \ offset = {:?}, data_in_buffer = {:?}, \ write_tracker = {:?}", - obscured_file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle + obscured_file_handle, req_id, offset, data_in_buffer, @@ -489,7 +514,7 @@ where data_in_buffer.as_ref(), ) { Ok(_) => { - self.partial_write_request_tracker = Some(write_tracker); + self.partial_write_request_tracker = Some(write_tracker); // TODO: This might belong to return value } Err(e) => { error!("SFTP write thrown: {:?}", e); @@ -498,7 +523,10 @@ where }; } _ => { - error!("Packet type could not be handled {:?}", packet_type); + error!( + "RanOut of Packet type could not be handled {:?}", + packet_type + ); push_general_failure( ReqId(u32::MAX), "Unsupported Request: Too long", @@ -508,7 +536,6 @@ where }; Ok(()) } - } #[inline] diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 2760155d..7f5cf4a4 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -3,8 +3,9 @@ use crate::proto::{ SFTP_MINIMUM_PACKET_LEN, SFTP_WRITE_REQID_INDEX, SftpNum, }; use crate::sftphandle::PartialWriteRequestTracker; -use crate::{FileHandle, OpaqueFileHandle}; +use crate::{FileHandle, OpaqueFileHandle, SftpError, SftpResult}; +use sunset::error::RanOut; use sunset::sshwire::{BinString, SSHDecode, SSHSource, WireError, WireResult}; #[allow(unused_imports)] @@ -53,7 +54,7 @@ impl<'de> SftpSource<'de> { /// /// **Warning**: will only work in well formed packets, in other case the result will contain garbage pub(crate) fn peak_packet_type(&self) -> WireResult { - if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + if self.buffer.len() < SFTP_FIELD_ID_INDEX { Err(WireError::RanOut) } else { Ok(SftpNum::from(self.buffer[SFTP_FIELD_ID_INDEX])) @@ -87,15 +88,15 @@ impl<'de> SftpSource<'de> { T: OpaqueFileHandle, >( &mut self, - ) -> WireResult<(T, ReqId, u64, BinString<'de>, PartialWriteRequestTracker)> + ) -> SftpResult<(T, ReqId, u64, BinString<'de>, PartialWriteRequestTracker)> { if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - return Err(WireError::PacketWrong); // TODO: Find a better error + return Err(WireError::RanOut.into()); } match self.peak_packet_type()? { SftpNum::SSH_FXP_WRITE => {} - _ => return Err(WireError::PacketWrong), // TODO: Find a better error + _ => return Err(SftpError::NotSupported), }; self.index = SFTP_WRITE_REQID_INDEX; From 1b26c503589c20c3b3c1415db0cd0a5da4d6dbbc Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 10:50:53 +1000 Subject: [PATCH 116/393] WIP: Fixing bugs RequestHolder Transition logic --- sftp/src/sftphandle.rs | 62 ++++++++++++++++++++++++------------------ sftp/src/sftpsource.rs | 2 +- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index c625f4b9..66bc4c3d 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -125,19 +125,21 @@ where let mut sink = SftpSink::new(&mut buffer_out[used_out_accumulated_index..]); - debug!("SFTP Process State: {:?}", self.state); + debug!( + "<=======================[ SFTP Process State: {:?} ]=======================>", + self.state + ); match &self.state { SftpHandleState::Fragmented(fragment_case) => { match fragment_case { FragmentedRequestState::ProcessingClippedRequest => { - let appending_result = incomplete_request_holder + if let Err(e) = incomplete_request_holder .try_append_for_valid_request( // TODO: All your problems are here. Focus &buffer_in[buffer_in_remaining_index..], - ); - - if let Err(e) = appending_result { + ) + { match e { RequestHolderError::RanOut => { warn!( @@ -148,21 +150,23 @@ where incomplete_request_holder.appended(); continue; } - RequestHolderError::NoRoom => { - // Fragmented Write request... - warn!( - "There is not enough room in incomplete request holder \ - to accommodate this packet buffer." - ) - } RequestHolderError::WireError( WireError::RanOut, ) => { warn!( - "WireError: There is not enough room in incomplete request holder \ - to accommodate this packet buffer." + "WIRE ERROR: There was not enough bytes in the buffer_in. \ + We will continue adding bytes" + ); + buffer_in_remaining_index += + incomplete_request_holder.appended(); + continue; + } + RequestHolderError::NoRoom => { + warn!( + "The request holder if full but the request in incomplete" ) } + _ => { error!( "Unhandled error completing incomplete request {:?}", @@ -185,10 +189,7 @@ where ); trace!("Internal Source Content: {:?}", source); - let decoding_request_result = - SftpPacket::decode_request(&mut source); - - match decoding_request_result { + match SftpPacket::decode_request(&mut source) { Ok(request) => { self.handle_general_request(&mut sink, request)?; incomplete_request_holder.reset(); @@ -200,9 +201,8 @@ where .handle_ran_out(&mut sink, &mut source) { Ok(_) => { - self.state = - SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); incomplete_request_holder.reset(); + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); } Err(e) => match e { _ => { @@ -210,9 +210,9 @@ where "handle_ran_out finished with error: {:?}", e ); - return (Err( + return Err( SunsetError::Bug.into() - )); + ); } }, } @@ -367,17 +367,22 @@ where self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) } - Err(e) => match e { + Err(e) => { + error!("Error handle_ran_out"); + match e { SftpError::WireError(WireError::RanOut) => { + let read = incomplete_request_holder .try_hold( &buffer_in [buffer_in_remaining_index..], - )?; // Fails because it does not fit. Also. It is not the beginning of a new packet - self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest) + )?; + buffer_in_remaining_index += read; + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); + continue; } _ => return (Err(SunsetError::Bug.into())), - }, + }}, }; } WireError::UnknownPacket { number: _ } => { @@ -488,7 +493,7 @@ where let packet_type = source.peak_packet_type()?; match packet_type { SftpNum::SSH_FXP_WRITE => { - debug!("about to decode packet partial write content",); + debug!("about to decode packet partial write content. Source remaining = {:?}",source.remaining()); let ( obscured_file_handle, req_id, @@ -514,6 +519,9 @@ where data_in_buffer.as_ref(), ) { Ok(_) => { + debug!( + "Storing a write tracker for a fragmented write request" + ); self.partial_write_request_tracker = Some(write_tracker); // TODO: This might belong to return value } Err(e) => { diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 7f5cf4a4..f7f023ad 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -54,7 +54,7 @@ impl<'de> SftpSource<'de> { /// /// **Warning**: will only work in well formed packets, in other case the result will contain garbage pub(crate) fn peak_packet_type(&self) -> WireResult { - if self.buffer.len() < SFTP_FIELD_ID_INDEX { + if self.buffer.len() < SFTP_FIELD_ID_INDEX + 1 { Err(WireError::RanOut) } else { Ok(SftpNum::from(self.buffer[SFTP_FIELD_ID_INDEX])) From 55dd5b2874656970e521504e4f97eb4500126ca4 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 11:39:29 +1000 Subject: [PATCH 117/393] WIP: Successfully received 100MB and 1024MB It is time to tidy up --- demo/sftp/std/testing/test_limit_write_req.sh | 33 ---------------- .../std/testing/test_long_write_requests.sh | 38 +++++++++++++++++++ demo/sftp/std/testing/test_write_requests.sh | 7 ++-- sftp/src/requestholder.rs | 4 +- 4 files changed, 44 insertions(+), 38 deletions(-) delete mode 100755 demo/sftp/std/testing/test_limit_write_req.sh create mode 100755 demo/sftp/std/testing/test_long_write_requests.sh diff --git a/demo/sftp/std/testing/test_limit_write_req.sh b/demo/sftp/std/testing/test_limit_write_req.sh deleted file mode 100755 index 45614183..00000000 --- a/demo/sftp/std/testing/test_limit_write_req.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -# Set remote server details -REMOTE_HOST="192.168.69.2" -REMOTE_USER="any" - -# Generate random data files -echo "Generating random data files..." - -dd if=/dev/random bs=1024 count=65 of=./TwoWriteRequests_random 2>/dev/null # Fails - - -echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." -echo "Test Results:" -echo "=============" - -sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF -put ./TwoWriteRequests_random -bye -EOF - -diff ./TwoWriteRequests_random ./out/TwoWriteRequests_random -if [ $? -eq 0 ]; then - echo "PASS" -else - echo "FAIL" -fi - -echo "Cleaning up local files..." -rm -f ./*_random -rm -f ./out/*_random - -echo "Upload test completed." \ No newline at end of file diff --git a/demo/sftp/std/testing/test_long_write_requests.sh b/demo/sftp/std/testing/test_long_write_requests.sh new file mode 100755 index 00000000..a0104e17 --- /dev/null +++ b/demo/sftp/std/testing/test_long_write_requests.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Define test files +FILES=("100MB_random" "1024MB_random") + +# Generate random data files +echo "Generating random data files..." +dd if=/dev/random bs=1048576 count=100 of=./100MB_random 2>/dev/null +dd if=/dev/random bs=1048576 count=1024 of=./1024MB_random 2>/dev/null + +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." + +# Upload all files +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +$(printf 'put ./%s\n' "${FILES[@]}") +bye +EOF + +echo "Test Results:" +echo "=============" + +# Test each file +for file in "${FILES[@]}"; do + if diff "./${file}" "./out/${file}" >/dev/null 2>&1; then + echo "PASS: ${file}" + else + echo "FAIL: ${file}" + fi +done + +echo "Cleaning up local files..." +rm -f ./*_random ./out/*_random + +echo "Upload test completed." \ No newline at end of file diff --git a/demo/sftp/std/testing/test_write_requests.sh b/demo/sftp/std/testing/test_write_requests.sh index 8ac5d5e7..13a4dbcc 100755 --- a/demo/sftp/std/testing/test_write_requests.sh +++ b/demo/sftp/std/testing/test_write_requests.sh @@ -5,7 +5,7 @@ REMOTE_HOST="192.168.69.2" REMOTE_USER="any" # Define test files -FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random" "256kB_random" "1MB_random" "2MB_random") +FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random" "256kB_random" "1024kB_random" "2048kB_random") # Generate random data files echo "Generating random data files..." @@ -14,8 +14,9 @@ dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null dd if=/dev/random bs=1024 count=65 of=./65kB_random 2>/dev/null dd if=/dev/random bs=1024 count=256 of=./256kB_random 2>/dev/null -dd if=/dev/random bs=1048576 count=1 of=./1MB_random 2>/dev/null -dd if=/dev/random bs=1048576 count=2 of=./2MB_random 2>/dev/null +dd if=/dev/random bs=1024 count=1024 of=./1024kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=2048 of=./2048kB_random 2>/dev/null + echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs index 36f37fa3..af35e396 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/requestholder.rs @@ -197,13 +197,13 @@ impl<'a> RequestHolder<'a> { return Err(RequestHolderError::RanOut); } - let complete_to_id_index = proto::SFTP_FIELD_ID_INDEX + let complete_to_id_index = (proto::SFTP_FIELD_ID_INDEX + 1) .checked_sub(self.buffer_fill_index) .unwrap_or(0); if complete_to_id_index > 0 { warn!( - "The held fragment len = {:?}, is insufficient to peak\ + "The held fragment len = {:?}, is insufficient to peak \ the length and type. Will append {:?} to reach the \ id field index: {:?}", self.buffer_fill_index, From 89482b4c82020e69ad908fd04d3ec21c8267fc31 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 11:50:25 +1000 Subject: [PATCH 118/393] WIP: Added SftpError::FileServerError(StatusCode) + others - Reasonable warning removal - Adding comments - Removing dead code - More refactoring --- demo/sftp/std/src/main.rs | 14 +-- sftp/src/lib.rs | 2 - sftp/src/requestholder.rs | 5 +- sftp/src/sftperror.rs | 36 +++++-- sftp/src/sftphandle.rs | 213 +++++++++++++++++++++++++------------- sftp/src/sftpsource.rs | 5 - 6 files changed, 174 insertions(+), 101 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index b70e9046..74403ad6 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -1,6 +1,6 @@ use sunset::*; use sunset_async::{ProgressHolder, SSHServer, SunsetMutex, SunsetRawMutex}; -use sunset_sftp::{RequestHolder, SftpHandler}; +use sunset_sftp::SftpHandler; pub(crate) use sunset_demo_common as demo_common; @@ -141,6 +141,7 @@ impl DemoServer for StdDemo { Ok(()) }; + #[allow(unreachable_code)] let sftp_loop = async { loop { let ch = chan_pipe.receive().await; @@ -152,9 +153,6 @@ impl DemoServer for StdDemo { let mut buffer_out = [0u8; 384]; let mut incomplete_request_buffer = [0u8; 128]; // TODO: Find a non arbitrary length - let mut incomplete_request_holder = - RequestHolder::new(&mut incomplete_request_buffer); - match { let mut stdio = serv.stdio(ch).await?; let mut file_server = DemoSftpServer::new( @@ -164,7 +162,7 @@ impl DemoServer for StdDemo { let mut sftp_handler = SftpHandler::::new( &mut file_server, - // &mut incomplete_request_buffer, + &mut incomplete_request_buffer, ); loop { let lr = stdio.read(&mut buffer_in).await?; @@ -175,11 +173,7 @@ impl DemoServer for StdDemo { } let lw = sftp_handler - .process( - &buffer_in[0..lr], - &mut incomplete_request_holder, - &mut buffer_out, - ) + .process(&buffer_in[0..lr], &mut buffer_out) .await?; if lw > 0 { stdio.write(&mut buffer_out[0..lw]).await?; diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index ee2019f4..0d667c01 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -13,8 +13,6 @@ pub use sftpserver::ReadReply; pub use sftpserver::SftpOpResult; pub use sftpserver::SftpServer; -pub use requestholder::RequestHolder; - pub use sftphandle::SftpHandler; pub use opaquefilehandle::{OpaqueFileHandle, OpaqueFileHandleManager, PathFinder}; diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs index af35e396..1042d2d2 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/requestholder.rs @@ -1,11 +1,11 @@ -use crate::{proto, sftperror, sftpsource::SftpSource}; +use crate::{proto, sftpsource::SftpSource}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; use sunset::sshwire::WireError; #[derive(Debug)] -pub(crate) enum RequestHolderError { +pub enum RequestHolderError { /// The slice to hold is too long NoRoom, /// The slice holder is keeping a slice already. Consideer cleaning @@ -283,7 +283,6 @@ impl<'a> RequestHolder<'a> { return Err(RequestHolderError::RanOut); } } - Ok(()) } /// Gets a reference to the slice that it is holding diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index a97d52fd..b957fc62 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -1,21 +1,37 @@ -use core::convert::From; +use crate::{StatusCode, requestholder::RequestHolderError}; -use log::warn; use sunset::Error as SunsetError; use sunset::sshwire::WireError; -use crate::{SftpOpResult, StatusCode, requestholder::RequestHolderError}; +use core::convert::From; +use log::warn; +// TODO: Use it more broadly where reasonable +/// Errors that are specific to this SFTP lib #[derive(Debug)] pub enum SftpError { + /// The SFTP server has not been initialised. No SFTP version has been + /// establish NotInitialized, + /// An `SSH_FXP_INIT` packet was received after the server was already + /// initialized AlreadyInitialized, + /// A packet could not be decoded as it was malformed MalformedPacket, + /// The server does not have an implementation for the current request. + /// Some possible causes are: + /// + /// - The request has not been handled by an [`crate::sftpserver::SftpServer`] + /// - Long request which its handling was not implemented NotSupported, + /// The [`crate::sftpserver::SftpServer`] failed doing an IO operation + FileServerError(StatusCode), + /// A RequestHolder instance throw an error. See [`crate::requestholder::RequestHolderError`] + RequestHolderError(RequestHolderError), + /// A variant containing a [`WireError`] WireError(WireError), - OperationError(StatusCode), + /// A variant containing a [`SunsetError`] SunsetError(SunsetError), - RequestHolderError(RequestHolderError), } impl From for SftpError { @@ -72,13 +88,17 @@ impl From for SunsetError { warn!("Casting error loosing information: {:?}", value); SunsetError::PacketWrong {} } - SftpError::OperationError(_) => { + SftpError::RequestHolderError(_) => { warn!("Casting error loosing information: {:?}", value); - SunsetError::PacketWrong {} + SunsetError::Bug + } + SftpError::FileServerError(_) => { + warn!("Casting error loosing information: {:?}", value); + SunsetError::Bug } - SftpError::RequestHolderError(request_holder_error) => SunsetError::Bug, } } } +/// result specific to this SFTP lib pub type SftpResult = Result; diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 66bc4c3d..e440e42f 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,6 +1,6 @@ use crate::proto::{ - self, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, SFTP_MINIMUM_PACKET_LEN, - SFTP_VERSION, SftpNum, SftpPacket, Status, StatusCode, + self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, + SftpPacket, Status, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; use crate::sftperror::SftpResult; @@ -10,13 +10,13 @@ use crate::sftpsource::SftpSource; use crate::{OpaqueFileHandle, SftpError}; use sunset::Error as SunsetError; -use sunset::Error; use sunset::sshwire::{SSHSource, WireError, WireResult}; use core::{u32, usize}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; +/// FSM for handling sftp requests during `process()` #[derive(Default, Debug, PartialEq, Eq)] enum SftpHandleState { /// The handle is not initialized @@ -28,15 +28,20 @@ enum SftpHandleState { Fragmented(FragmentedRequestState), } +/// FSM subset to handle fragmented request as part of `SftpHandleState` #[derive(Debug, PartialEq, Eq)] enum FragmentedRequestState { - /// A request that is clipped needs to be process. It cannot be decoded and more bytes are needed + /// A request that is clipped needs to be process. It cannot be decoded + /// and more bytes are needed ProcessingClippedRequest, - /// A request, with a length over the incoming buffer capacity is being processed + /// A request, with a length over the incoming buffer capacity is being + /// processed ProcessingLongRequest, } -/// Used to keep record of a long SFTP Write request that does not fit in receiving buffer and requires processing in batches +// TODO: Generalize this to allow other request types +/// Used to keep record of a long SFTP Write request that does not fit in +/// receiving buffer and requires processing in batches #[derive(Debug)] pub struct PartialWriteRequestTracker { req_id: ReqId, @@ -46,6 +51,7 @@ pub struct PartialWriteRequestTracker { } impl PartialWriteRequestTracker { + /// Creates a new PartialWriteRequestTracker pub fn new( req_id: ReqId, opaque_handle: T, @@ -59,15 +65,18 @@ impl PartialWriteRequestTracker { remain_data_offset, }) } - + /// Returns the opaque file handle associated with the request + /// tracked pub fn get_opaque_file_handle(&self) -> T { self.opaque_handle.clone() } } -/// Process the raw buffers in and out from a subsystem channel decoding request and encoding responses +/// Process the raw buffers in and out from a subsystem channel decoding +/// request and encoding responses /// -/// It will delegate request to an `SftpServer` implemented by the library user taking into account the local system details. +/// It will delegate request to an `SftpServer` implemented by the library +/// user taking into account the local system details. pub struct SftpHandler<'a, T, S> where T: OpaqueFileHandle, @@ -76,14 +85,15 @@ where /// Holds the internal state if the SFTP handle state: SftpHandleState, - /// The local SFTP File server implementing the basic SFTP requests defined by `SftpServer` + /// The local SFTP File server implementing the basic SFTP requests + /// defined by `SftpServer` file_server: &'a mut S, - /// Once the client and the server have verified the agreed SFTP version the session is initialized - initialized: bool, - - /// Use to process SFTP Write packets that have been received partially and the remaining is expected in successive buffers + /// Use to process SFTP Write packets that have been received + /// partially and the remaining is expected in successive buffers partial_write_request_tracker: Option>, + + incomplete_request_holder: RequestHolder<'a>, } impl<'a, T, S> SftpHandler<'a, T, S> @@ -91,21 +101,39 @@ where T: OpaqueFileHandle, S: SftpServer<'a, T>, { - pub fn new(file_server: &'a mut S) -> Self { + /// Creates a new instance of the structure. + /// + /// Requires: + /// + /// - `file_server` (implementing `SftpServer`): to execute + /// the request in the local system + /// - `incomplete_request_buffer`: used to deal with fragmented + /// packets during `process()` + pub fn new( + file_server: &'a mut S, + incomplete_request_buffer: &'a mut [u8], + ) -> Self { SftpHandler { file_server, - initialized: false, partial_write_request_tracker: None, state: SftpHandleState::default(), + incomplete_request_holder: RequestHolder::new(incomplete_request_buffer), } } - /// Decodes the buffer_in request, process the request delegating operations to an Struct implementing SftpServer, - /// serializes an answer in buffer_out and **returns** the length used in buffer_out + /// Maintaining an internal status: + /// + /// - Decodes the buffer_in request + /// - Process the request delegating + /// operations to a `SftpServer` implementation + /// - Serializes an answer in buffer_out + /// + /// **Returns**: A result containing the number of bytes used in + /// `buffer_out` pub async fn process( &mut self, buffer_in: &[u8], - incomplete_request_holder: &mut RequestHolder<'_>, + // incomplete_request_holder: &mut RequestHolder<'_>, buffer_out: &mut [u8], ) -> SftpResult { let in_len = buffer_in.len(); @@ -134,7 +162,8 @@ where SftpHandleState::Fragmented(fragment_case) => { match fragment_case { FragmentedRequestState::ProcessingClippedRequest => { - if let Err(e) = incomplete_request_holder + if let Err(e) = self + .incomplete_request_holder .try_append_for_valid_request( // TODO: All your problems are here. Focus &buffer_in[buffer_in_remaining_index..], @@ -146,8 +175,9 @@ where "There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); - buffer_in_remaining_index += - incomplete_request_holder.appended(); + buffer_in_remaining_index += self + .incomplete_request_holder + .appended(); continue; } RequestHolderError::WireError( @@ -157,8 +187,9 @@ where "WIRE ERROR: There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); - buffer_in_remaining_index += - incomplete_request_holder.appended(); + buffer_in_remaining_index += self + .incomplete_request_holder + .appended(); continue; } RequestHolderError::NoRoom => { @@ -181,27 +212,36 @@ where ); } - let used = incomplete_request_holder.appended(); + let used = self.incomplete_request_holder.appended(); buffer_in_remaining_index += used; let mut source = SftpSource::new( - &incomplete_request_holder.try_get_ref()?, + &self.incomplete_request_holder.try_get_ref()?, ); trace!("Internal Source Content: {:?}", source); match SftpPacket::decode_request(&mut source) { Ok(request) => { - self.handle_general_request(&mut sink, request)?; - incomplete_request_holder.reset(); + Self::handle_general_request( + &mut self.file_server, + &mut sink, + request, + )?; + self.incomplete_request_holder.reset(); self.state = SftpHandleState::Idle; } Err(e) => match e { WireError::RanOut => { - match self - .handle_ran_out(&mut sink, &mut source) - { - Ok(_) => { - incomplete_request_holder.reset(); + match Self::handle_ran_out( + &mut self.file_server, + &mut sink, + &mut source, + ) { + Ok(holder) => { + self.partial_write_request_tracker = + Some(holder); + self.incomplete_request_holder + .reset(); self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); } Err(e) => match e { @@ -258,9 +298,13 @@ where let data_segment = // Fails!! source.dec_as_binstring(usable_data)?; - let data_segment_len = - u32::try_from(data_segment.0.len()) - .map_err(|e| SunsetError::Bug)?; + let data_segment_len = u32::try_from( + data_segment.0.len(), + ) + .map_err(|e| { + error!("Error casting data segment len to u32: {e}"); + SunsetError::Bug + })?; let current_write_offset = write_tracker.remain_data_offset; write_tracker.remain_data_offset += @@ -323,7 +367,7 @@ where SftpHandleState::Initializing => match sftp_packet { Ok(request) => { match request { - SftpPacket::Init(init_version_client) => { + SftpPacket::Init(_) => { let version = SftpPacket::Version(InitVersionLowest { version: SFTP_VERSION, @@ -353,7 +397,12 @@ where SftpHandleState::Idle => { match sftp_packet { Ok(request) => { - self.handle_general_request(&mut sink, request)? + // self.handle_general_request(&mut sink, request)? + Self::handle_general_request( + &mut self.file_server, + &mut sink, + request, + )?; } Err(e) => match e { WireError::RanOut => { @@ -362,27 +411,40 @@ where e ); - match self.handle_ran_out(&mut sink, &mut source) { - Ok(_) => { - self.state = + match Self::handle_ran_out( + &mut self.file_server, + &mut sink, + &mut source, + ) { + Ok(holder) => { + self.partial_write_request_tracker = + Some(holder); + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) - } - Err(e) => { - error!("Error handle_ran_out"); - match e { - SftpError::WireError(WireError::RanOut) => { - - let read = incomplete_request_holder + } + Err(e) => { + error!("Error handle_ran_out"); + match e { + SftpError::WireError( + WireError::RanOut, + ) => { + let read = self.incomplete_request_holder .try_hold( &buffer_in [buffer_in_remaining_index..], - )?; - buffer_in_remaining_index += read; + )?; + buffer_in_remaining_index += + read; self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); continue; } - _ => return (Err(SunsetError::Bug.into())), - }}, + _ => { + return Err( + SunsetError::Bug.into(), + ); + } + } + } }; } WireError::UnknownPacket { number: _ } => { @@ -416,26 +478,27 @@ where } fn handle_general_request( - &mut self, + file_server: &mut S, sink: &mut SftpSink<'_>, request: SftpPacket<'_>, ) -> Result<(), SftpError> where T: OpaqueFileHandle, { - Ok(match request { - SftpPacket::Init(init_version_client) => { - return Err(SftpError::MalformedPacket); + match request { + SftpPacket::Init(_) => { + error!("The Init packet is not a request but an initialization"); + return Err(SftpError::AlreadyInitialized); } SftpPacket::PathInfo(req_id, path_info) => { - let a_name = self.file_server.realpath(path_info.path.as_str()?)?; + let a_name = file_server.realpath(path_info.path.as_str()?)?; let response = SftpPacket::Name(req_id, a_name); response.encode_response(sink)?; } SftpPacket::Open(req_id, open) => { - match self.file_server.open(open.filename.as_str()?, &open.attrs) { + match file_server.open(open.filename.as_str()?, &open.attrs) { Ok(opaque_file_handle) => { let response = SftpPacket::Handle( req_id, @@ -453,7 +516,7 @@ where }; } SftpPacket::Write(req_id, write) => { - match self.file_server.write( + match file_server.write( &T::try_from(&write.handle)?, write.offset, write.data.as_ref(), @@ -466,7 +529,7 @@ where }; } SftpPacket::Close(req_id, close) => { - match self.file_server.close(&T::try_from(&close.handle)?) { + match file_server.close(&T::try_from(&close.handle)?) { Ok(_) => push_ok(req_id, sink)?, Err(e) => { error!("SFTP Close thrown: {:?}", e); @@ -478,29 +541,35 @@ where error!("Unsuported request type"); push_unsupported(ReqId(0), sink)?; } - }) + } + Ok(()) } + // TODO: Handle more long requests /// Handles long request that do not fit in the buffers and stores a tracker /// /// **WARNING:** Only `SSH_FXP_WRITE` has been implemented! /// fn handle_ran_out( - &mut self, + file_server: &mut S, sink: &mut SftpSink<'_>, source: &mut SftpSource<'_>, - ) -> SftpResult<()> { + ) -> SftpResult> { let packet_type = source.peak_packet_type()?; match packet_type { SftpNum::SSH_FXP_WRITE => { - debug!("about to decode packet partial write content. Source remaining = {:?}",source.remaining()); + debug!( + "about to decode packet partial write content. Source remaining = {:?}", + source.remaining() + ); let ( obscured_file_handle, req_id, offset, data_in_buffer, write_tracker, - ) = source.dec_packet_partial_write_content_and_get_tracker()?; + ) = source + .dec_packet_partial_write_content_and_get_tracker::()?; trace!( "obscured_file_handle = {:?}, req_id = {:?}, \ @@ -513,7 +582,7 @@ where write_tracker, ); - match self.file_server.write( + match file_server.write( &obscured_file_handle, offset, data_in_buffer.as_ref(), @@ -522,11 +591,13 @@ where debug!( "Storing a write tracker for a fragmented write request" ); - self.partial_write_request_tracker = Some(write_tracker); // TODO: This might belong to return value + return Ok(write_tracker); + // partial_write_request_tracker = Some(write_tracker); // TODO: This might belong to return value } Err(e) => { error!("SFTP write thrown: {:?}", e); push_general_failure(req_id, "error writing ", sink)?; + return Err(SftpError::FileServerError(e)); } }; } @@ -535,14 +606,10 @@ where "RanOut of Packet type could not be handled {:?}", packet_type ); - push_general_failure( - ReqId(u32::MAX), - "Unsupported Request: Too long", - sink, - )?; + return Err(SftpError::NotSupported); } }; - Ok(()) + // Ok(()) } } diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index f7f023ad..e1c75b55 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -5,7 +5,6 @@ use crate::proto::{ use crate::sftphandle::PartialWriteRequestTracker; use crate::{FileHandle, OpaqueFileHandle, SftpError, SftpResult}; -use sunset::error::RanOut; use sunset::sshwire::{BinString, SSHDecode, SSHSource, WireError, WireResult}; #[allow(unused_imports)] @@ -136,8 +135,4 @@ impl<'de> SftpSource<'de> { ) -> WireResult> { Ok(BinString(self.take(len)?)) } - - pub(crate) fn buffer_ref(&self) -> &[u8] { - self.buffer.clone() - } } From f654b4479625e11f977243064fae95c92c2d3f71 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 14:09:28 +1000 Subject: [PATCH 119/393] WIP: sftphandle->sftphandler fixing sftperror statuscode casting --- sftp/src/lib.rs | 4 ++-- sftp/src/sftperror.rs | 2 +- sftp/src/{sftphandle.rs => sftphandler.rs} | 17 +++++++++-------- sftp/src/sftpsource.rs | 2 +- 4 files changed, 13 insertions(+), 12 deletions(-) rename sftp/src/{sftphandle.rs => sftphandler.rs} (98%) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 0d667c01..bfa1e99b 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,7 +1,7 @@ mod opaquefilehandle; mod proto; mod requestholder; -mod sftphandle; +mod sftphandler; mod sftpserver; mod sftpsink; mod sftpsource; @@ -13,7 +13,7 @@ pub use sftpserver::ReadReply; pub use sftpserver::SftpOpResult; pub use sftpserver::SftpServer; -pub use sftphandle::SftpHandler; +pub use sftphandler::SftpHandler; pub use opaquefilehandle::{OpaqueFileHandle, OpaqueFileHandleManager, PathFinder}; diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index b957fc62..13d6c333 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -48,7 +48,7 @@ impl From for SftpError { impl From for SftpError { fn from(value: StatusCode) -> Self { - SftpError::OperationError(value) + SftpError::FileServerError(value) } } diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandler.rs similarity index 98% rename from sftp/src/sftphandle.rs rename to sftp/src/sftphandler.rs index e440e42f..c0fd7674 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandler.rs @@ -16,7 +16,7 @@ use core::{u32, usize}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -/// FSM for handling sftp requests during `process()` +/// FSM for handling sftp requests during [`SftpHandler::process`] #[derive(Default, Debug, PartialEq, Eq)] enum SftpHandleState { /// The handle is not initialized @@ -28,7 +28,7 @@ enum SftpHandleState { Fragmented(FragmentedRequestState), } -/// FSM subset to handle fragmented request as part of `SftpHandleState` +/// FSM subset to handle fragmented request as part of [`SftpHandleState`] #[derive(Debug, PartialEq, Eq)] enum FragmentedRequestState { /// A request that is clipped needs to be process. It cannot be decoded @@ -51,7 +51,7 @@ pub struct PartialWriteRequestTracker { } impl PartialWriteRequestTracker { - /// Creates a new PartialWriteRequestTracker + /// Creates a new [`PartialWriteRequestTracker`] pub fn new( req_id: ReqId, opaque_handle: T, @@ -75,7 +75,8 @@ impl PartialWriteRequestTracker { /// Process the raw buffers in and out from a subsystem channel decoding /// request and encoding responses /// -/// It will delegate request to an `SftpServer` implemented by the library +/// It will delegate request to an [`crate::sftpserver::SftpServer`] +/// implemented by the library /// user taking into account the local system details. pub struct SftpHandler<'a, T, S> where @@ -86,7 +87,7 @@ where state: SftpHandleState, /// The local SFTP File server implementing the basic SFTP requests - /// defined by `SftpServer` + /// defined by [`crate::sftpserver::SftpServer`] file_server: &'a mut S, /// Use to process SFTP Write packets that have been received @@ -105,10 +106,10 @@ where /// /// Requires: /// - /// - `file_server` (implementing `SftpServer`): to execute + /// - `file_server` (implementing [`crate::sftpserver::SftpServer`] ): to execute /// the request in the local system /// - `incomplete_request_buffer`: used to deal with fragmented - /// packets during `process()` + /// packets during [`SftpHandler::process`] pub fn new( file_server: &'a mut S, incomplete_request_buffer: &'a mut [u8], @@ -125,7 +126,7 @@ where /// /// - Decodes the buffer_in request /// - Process the request delegating - /// operations to a `SftpServer` implementation + /// operations to a [`SftpHandler::process`] implementation /// - Serializes an answer in buffer_out /// /// **Returns**: A result containing the number of bytes used in diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index e1c75b55..94142c78 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -2,7 +2,7 @@ use crate::proto::{ ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, SFTP_MINIMUM_PACKET_LEN, SFTP_WRITE_REQID_INDEX, SftpNum, }; -use crate::sftphandle::PartialWriteRequestTracker; +use crate::sftphandler::PartialWriteRequestTracker; use crate::{FileHandle, OpaqueFileHandle, SftpError, SftpResult}; use sunset::sshwire::{BinString, SSHDecode, SSHSource, WireError, WireResult}; From 0bdb7c42890fcaa41c4560934897f66a9fb42592 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 14:17:30 +1000 Subject: [PATCH 120/393] WIP: Removing dead code and adding comments --- sftp/src/lib.rs | 3 +++ sftp/src/requestholder.rs | 46 +-------------------------------------- sftp/src/sftpserver.rs | 7 ++++-- sftp/src/sftpsink.rs | 15 +++++++++++-- sftp/src/sftpsource.rs | 40 +++++++++++++++++++++------------- 5 files changed, 47 insertions(+), 64 deletions(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index bfa1e99b..1ed6689e 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,3 +1,6 @@ +#![forbid(unsafe_code)] +#![warn(missing_docs)] + mod opaquefilehandle; mod proto; mod requestholder; diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs index 1042d2d2..ed0abf4e 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/requestholder.rs @@ -12,8 +12,6 @@ pub enum RequestHolderError { Busy, /// The slice holder is empty Empty, - /// The instance has been invalidated - Invalid, /// There is not enough data in the slice we are trying to add. we need more data RanOut, /// WireError @@ -50,8 +48,6 @@ pub(crate) type RequestHolderResult = Result; /// /// - `reset`: reset counters and flags to allow `try_hold` a new request /// -/// - **OR** `invalidate`: return the reference to the slice provided in `new` -/// and mark the structure as invalid. At this point it should be disposed #[derive(Debug)] pub struct RequestHolder<'a> { /// The buffer used to contain the data for the request @@ -60,8 +56,6 @@ pub struct RequestHolder<'a> { buffer_fill_index: usize, /// Number of bytes appended in a previous `try_hold` or `try_append_for_valid_request` slice appended: usize, - /// Marks the structure as invalid - invalid: bool, /// Used to mark when the structure is holding data busy: bool, } @@ -73,7 +67,6 @@ impl<'a> RequestHolder<'a> { RequestHolder { buffer: buffer, buffer_fill_index: 0, - invalid: false, // TODO: Remove if invalidate() is removed busy: false, appended: 0, } @@ -91,15 +84,10 @@ impl<'a> RequestHolder<'a> { /// - Ok(usize): the number of bytes read from the slice /// /// - `Err(Busy)`: If there has been a call to `try_hold` without a call to `reset` - /// - /// - `Err(Invalid)`: If the structure has been marked as invalid previously pub fn try_hold(&mut self, slice: &[u8]) -> RequestHolderResult { if self.busy { return Err(RequestHolderError::Busy); } - if self.invalid { - return Err(RequestHolderError::Invalid); - } self.busy = true; self.try_append_slice(slice)?; @@ -115,26 +103,10 @@ impl<'a> RequestHolder<'a> { /// Will not clear the previous data from the buffer. pub fn reset(&mut self) -> () { self.busy = false; - self.invalid = false; self.buffer_fill_index = 0; self.appended = 0; } - // TODO: Remove it if it is not used - /// Invalidates the current instance and returns its original buffer. Does not erase previous data - pub fn invalidate(&mut self) -> RequestHolderResult<&[u8]> { - if !self.busy { - return Err(RequestHolderError::Empty); - } - if self.invalid { - return Err(RequestHolderError::Invalid); - } - - self.invalid = true; - // self.buffer_fill_index = 0; - Ok(&self.buffer) - } - /// Using the content of the `RequestHolder` tries to find a valid /// SFTP request appending from slice into the internal buffer to /// form a valid request. @@ -149,8 +121,6 @@ impl<'a> RequestHolder<'a> { /// /// - `Err(NoRoom)`: The internal buffer is full but there is not a full valid request in the buffer /// - /// - `Err(Invalid)`: If the structure has been marked as invalid previously - /// /// - `Err(Empty)`: If the structure has not been loaded with `try_hold` /// /// - `Err(Bug)`: An unexpected condition arises @@ -167,11 +137,6 @@ impl<'a> RequestHolder<'a> { slice.len() ); - if self.invalid { - error!("Request Holder is invalid"); - return Err(RequestHolderError::Invalid); - } - if !self.busy { error!("Request Holder is not busy"); return Err(RequestHolderError::Empty); @@ -287,10 +252,6 @@ impl<'a> RequestHolder<'a> { /// Gets a reference to the slice that it is holding pub fn try_get_ref(&self) -> RequestHolderResult<&[u8]> { - if self.invalid { - return Err(RequestHolderError::Invalid); - } - if self.busy { debug!( "Returning reference to: {:?}", @@ -306,6 +267,7 @@ impl<'a> RequestHolder<'a> { self.buffer_fill_index == self.buffer.len() } + #[allow(unused)] /// Returns true if it has a slice in its buffer pub fn is_busy(&self) -> bool { self.busy @@ -325,8 +287,6 @@ impl<'a> RequestHolder<'a> { /// /// - `Ok(())`: the slice was appended /// - /// - `Err(Invalid)`: If the structure has been marked as invalid previously - /// /// - `Err(Empty)`: If the structure has not been loaded with `try_hold` /// /// - `Err(NoRoom)`: The internal buffer is full but there is not a full valid request in the buffer @@ -339,10 +299,6 @@ impl<'a> RequestHolder<'a> { return Err(RequestHolderError::Empty); } - if self.invalid { - return Err(RequestHolderError::Invalid); - } - let in_len = slice.len(); if in_len > self.remaining_len() { return Err(RequestHolderError::NoRoom); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 201c7eb3..22f5bffb 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -5,6 +5,7 @@ use crate::{ use core::marker::PhantomData; +/// Result used to store the result of an Sftp Operation pub type SftpOpResult = core::result::Result; /// All trait functions are optional in the SFTP protocol. @@ -31,7 +32,7 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - + /// Reads from a file that has previously being opened for reading fn read( &mut self, opaque_file_handle: &T, @@ -45,7 +46,7 @@ where ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - + /// Writes to a file that has previously being opened for writing fn write( &mut self, opaque_file_handle: &T, @@ -61,11 +62,13 @@ where Ok(()) } + /// Opens a directory fn opendir(&mut self, dir: &str) -> SftpOpResult { log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } + /// Reads the list of items in a directory fn readdir( &mut self, opaque_file_handle: &T, diff --git a/sftp/src/sftpsink.rs b/sftp/src/sftpsink.rs index f3bb85c3..ce55d265 100644 --- a/sftp/src/sftpsink.rs +++ b/sftp/src/sftpsink.rs @@ -5,6 +5,11 @@ use sunset::sshwire::{SSHSink, WireError}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; +/// A implementation fo [`SSHSink`] that observes some constraints for +/// SFTP packets +/// +/// **Important**: It needs to be [`SftpSink::finalize`] to add the packet +/// len #[derive(Default)] pub struct SftpSink<'g> { pub buffer: &'g mut [u8], @@ -12,13 +17,19 @@ pub struct SftpSink<'g> { } impl<'g> SftpSink<'g> { + /// Initializes the Sink, with the particularity that it will leave + /// [`crate::proto::SFTP_FIELD_LEN_LENGTH`] bytes empty at the + /// start of the buffer that will contain the total packet length + /// once the [`SftpSink::finalize`] method is called pub fn new(s: &'g mut [u8]) -> Self { SftpSink { buffer: s, index: SFTP_FIELD_LEN_LENGTH } } - /// Finalise the buffer by prepending the payload size and returning + /// Finalise the buffer by prepending the packet length field, + /// excluding the field itself. /// - /// Returns the final index in the buffer as a reference for the space used + /// **Returns** the final index in the buffer as a reference of the + /// space used pub fn finalize(&mut self) -> usize { if self.index <= SFTP_FIELD_LEN_LENGTH { warn!("SftpSink trying to terminate it before pushing data"); diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 94142c78..b1f0a2c0 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -10,15 +10,15 @@ use sunset::sshwire::{BinString, SSHDecode, SSHSource, WireError, WireResult}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -/// SftpSource implements SSHSource and also extra functions to handle some challenges with long SFTP packets in constrained environments +/// SftpSource implements [`SSHSource`] and also extra functions to handle +/// some challenges related to long SFTP packets in constrained environments #[derive(Default, Debug)] pub struct SftpSource<'de> { - pub buffer: &'de [u8], - pub index: usize, + buffer: &'de [u8], + index: usize, } impl<'de> SSHSource<'de> for SftpSource<'de> { - // Original take fn take(&mut self, len: usize) -> sunset::sshwire::WireResult<&'de [u8]> { if len + self.index > self.buffer.len() { return Err(WireError::RanOut); @@ -43,15 +43,19 @@ impl<'de> SSHSource<'de> for SftpSource<'de> { } impl<'de> SftpSource<'de> { + /// Creates a new [`SftpSource`] referencing a buffer pub fn new(buffer: &'de [u8]) -> Self { SftpSource { buffer: buffer, index: 0 } } - /// Peaks the buffer for packet type. This does not advance the reading index + /// Peaks the buffer for packet type [`SftpNum`]. This does not advance + /// the reading index /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// Useful to observe the packet fields in special conditions where a + /// `dec(s)` would fail /// - /// **Warning**: will only work in well formed packets, in other case the result will contain garbage + /// **Warning**: will only work in well formed packets, in other case + /// the result will contain garbage pub(crate) fn peak_packet_type(&self) -> WireResult { if self.buffer.len() < SFTP_FIELD_ID_INDEX + 1 { Err(WireError::RanOut) @@ -62,9 +66,11 @@ impl<'de> SftpSource<'de> { /// Peaks the buffer for packet length. This does not advance the reading index /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// Useful to observe the packet fields in special conditions where a `dec(s)` + /// would fail /// - /// **Warning**: will only work in well formed packets, in other case the result will contain garbage + /// **Warning**: will only work in well formed packets, in other case the result + /// will contain garbage pub(crate) fn peak_packet_len(&self) -> WireResult { if self.buffer.len() < SFTP_FIELD_LEN_INDEX + SFTP_FIELD_LEN_LENGTH { Err(WireError::RanOut) @@ -78,11 +84,13 @@ impl<'de> SftpSource<'de> { } } - /// Assuming that the buffer contains a Write request packet initial bytes and not its totality, - /// extracts a partial version of the write request and a Write request tracker to handle and a - /// tracker to continue processing subsequent portions of the request from a SftpSource + /// Assuming that the buffer contains a [`proto::Write`] request packet initial + /// bytes and not its totality, extracts a partial version of the write request + /// and a Write request tracker to handle and a tracker to continue processing + /// subsequent portions of the request from a SftpSource /// - /// **Warning**: will only work in well formed write packets, in other case the result will contain garbage + /// **Warning**: will only work in well formed write packets, in other case + /// the result will contain garbage pub(crate) fn dec_packet_partial_write_content_and_get_tracker< T: OpaqueFileHandle, >( @@ -126,9 +134,11 @@ impl<'de> SftpSource<'de> { Ok((obscured_file_handle, req_id, offset, data_in_buffer, write_tracker)) } - /// Used to decode a slice of SSHSource as a single BinString + /// Used to decode a slice of [`SSHSource`] as a single BinString /// - /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. + /// It will not use the first four bytes as u32 for length, instead + /// it will use the length of the data received and use it to set the + /// length of the returned BinString. pub(crate) fn dec_as_binstring( &mut self, len: usize, From 6c44b340093090df41f3b0cb3d4a01db7e9017b1 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 15:10:03 +1000 Subject: [PATCH 121/393] TODO: Use heapless::Vec instead of Vec for Name as it breaks the no_std premisses --- sftp/src/proto.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 0ed4bea0..4acb8125 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -135,6 +135,10 @@ pub struct NameEntry<'a> { pub attrs: Attrs, } +// TODO: Will a Vector be an issue for no_std? +// Maybe we should migrate this to heapless::Vec and let the user decide +// the number of elements via features flags? +/// A collection of [`NameEntry`] #[derive(Debug)] pub struct Name<'a>(pub Vec>); From 6f1a0e93333a73c22863d7f8e3275524bcd3977e Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 15:54:58 +1000 Subject: [PATCH 122/393] WIP: fixed bad tag ids --- sftp/src/sftphandler.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sftp/src/sftphandler.rs b/sftp/src/sftphandler.rs index c0fd7674..3c530a55 100644 --- a/sftp/src/sftphandler.rs +++ b/sftp/src/sftphandler.rs @@ -621,7 +621,7 @@ fn push_ok(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireError> { Status { code: StatusCode::SSH_FX_OK, message: "".into(), - lang: "EN".into(), + lang: "en-US".into(), }, ); trace!("Pushing an OK status message: {:?}", response); @@ -639,7 +639,7 @@ fn push_unsupported( Status { code: StatusCode::SSH_FX_OP_UNSUPPORTED, message: "Not implemented".into(), - lang: "EN".into(), + lang: "en-US".into(), }, ); debug!("Pushing a unsupported status message: {:?}", response); @@ -658,7 +658,7 @@ fn push_general_failure( Status { code: StatusCode::SSH_FX_FAILURE, message: msg.into(), - lang: "EN".into(), + lang: "en-US".into(), }, ); debug!("Pushing a general failure status message: {:?}", response); From 6239dd8b699c4f914b95363decf2b2ce4cfcacfd Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 15:55:42 +1000 Subject: [PATCH 123/393] WIP: More docs --- sftp/src/proto.rs | 58 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 4acb8125..bb04baf0 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -37,6 +37,7 @@ pub const SFTP_WRITE_REQID_INDEX: usize = 5; // pub const SFTP_WRITE_HANDLE_INDEX: usize = 9; // TODO is utf8 enough, or does this need to be an opaque binstring? +/// See [SSH_FXP_NAME in Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) #[derive(Debug, SSHEncode, SSHDecode)] pub struct Filename<'a>(TextString<'a>); @@ -46,12 +47,16 @@ impl<'a> From<&'a str> for Filename<'a> { } } +// TODO: standardize the encoding of filenames as str impl<'a> Filename<'a> { + /// pub fn as_str(&self) -> Result<&'a str, WireError> { core::str::from_utf8(self.0.0).map_err(|_| WireError::BadString) } } +/// An opaque handle that is used by the server to identify an open +/// file or folder. #[derive(Debug, Clone, Copy, PartialEq, Eq, SSHEncode, SSHDecode)] pub struct FileHandle<'a>(pub BinString<'a>); @@ -74,71 +79,107 @@ pub struct InitVersionLowest { // TODO variable number of ExtPair } +/// Used for `ssh_fxp_open` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Open<'a> { + /// The relative or absolute path of the file to be open pub filename: Filename<'a>, + /// File [permissions flags](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3) pub pflags: u32, + /// Initial attributes for the file pub attrs: Attrs, } +/// Used for `ssh_fxp_close` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Close<'a> { + /// An opaque handle that is used by the server to identify an open + /// file or folder to be closed. pub handle: FileHandle<'a>, } +/// Used for `ssh_fxp_read` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Read<'a> { + /// An opaque handle that is used by the server to identify an open + /// file or folder. pub handle: FileHandle<'a>, + /// The offset for the read operation pub offset: u64, + /// The number of bytes to be retrieved pub len: u32, } +/// Used for `ssh_fxp_write` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Write<'a> { + /// An opaque handle that is used by the server to identify an open + /// file or folder. pub handle: FileHandle<'a>, + /// The offset for the read operation pub offset: u64, + pub data: BinString<'a>, } // Responses +/// Used for `ssh_fxp_realpath` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.11). #[derive(Debug, SSHEncode, SSHDecode)] pub struct PathInfo<'a> { + /// The path pub path: TextString<'a>, } +/// Used for `ssh_fxp_status` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Status<'a> { + /// See [`StatusCode`] for possible codes pub code: StatusCode, + /// An extra message pub message: TextString<'a>, + /// A language tag as defined by [Tags for the Identification of Languages](https://datatracker.ietf.org/doc/html/rfc1766) pub lang: TextString<'a>, } - +/// Used for `ssh_fxp_handle` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7). #[derive(Debug, Clone, Copy, PartialEq, Eq, SSHEncode, SSHDecode)] pub struct Handle<'a> { + /// An opaque handle that is used by the server to identify an open + /// file or folder. pub handle: FileHandle<'a>, } +/// Used for `ssh_fxp_data` [responses](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Data<'a> { + /// Handle for the file referred pub handle: FileHandle<'a>, + /// Offset in the data read pub offset: u64, + /// raw data pub data: BinString<'a>, } +/// Struct to hold `SSH_FXP_NAME` response. +/// See [SSH_FXP_NAME in Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) #[derive(Debug, SSHEncode, SSHDecode)] pub struct NameEntry<'a> { + /// Is a file name being returned pub filename: Filename<'a>, /// longname is an undefined text line like "ls -l", /// SHOULD NOT be used. pub _longname: Filename<'a>, + /// Attributes for the file entry + /// + /// See [File Attributes](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-5) + /// for more information. pub attrs: Attrs, } // TODO: Will a Vector be an issue for no_std? // Maybe we should migrate this to heapless::Vec and let the user decide // the number of elements via features flags? -/// A collection of [`NameEntry`] +/// A collection of [`NameEntry`] used for [ssh_fxp_name responses](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7). #[derive(Debug)] pub struct Name<'a>(pub Vec>); @@ -183,9 +224,10 @@ pub struct ResponseAttributes { #[derive(Debug, SSHEncode, SSHDecode, Clone, Copy)] pub struct ReqId(pub u32); +/// For more information see [Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) #[derive(Debug, FromPrimitive, SSHEncode)] #[repr(u32)] -#[allow(non_camel_case_types)] +#[allow(non_camel_case_types, missing_docs)] pub enum StatusCode { #[sshwire(variant = "ssh_fx_ok")] SSH_FX_OK = 0, @@ -226,9 +268,12 @@ pub struct ExtPair<'a> { } /// Files attributes to describe Files as SFTP v3 specification +/// +/// See [File Attributes](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-5) +/// for more information. +#[allow(missing_docs)] #[derive(Debug, Default)] pub struct Attrs { - // flags: u32, defines used attributes pub size: Option, pub uid: Option, pub gid: Option, @@ -239,6 +284,7 @@ pub struct Attrs { // TODO extensions } +/// For more information see [File Attributes](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-5) #[repr(u32)] #[allow(non_camel_case_types)] pub enum AttrsFlags { @@ -263,6 +309,10 @@ impl core::ops::BitAnd for u32 { } impl Attrs { + /// Obtains the flags for the values stored in the [`Attrs`] struct. + /// + /// See [File Attributes](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-5) + /// for more information. pub fn flags(&self) -> u32 { let mut flags: u32 = 0; if self.size.is_some() { From c4fdfedde9f1be683afef4dd9f066eaf8c0ed54b Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 16:13:38 +1000 Subject: [PATCH 124/393] WIP: Refactoring SFTP lib and added comments from flat structure to a more conceptual structure Also added a brief header doc to show what is the intent and the status of the module. --- demo/sftp/std/src/demofilehandlemanager.rs | 5 +-- demo/sftp/std/src/demoopaquefilehandle.rs | 3 +- demo/sftp/std/src/demosftpserver.rs | 7 ++- demo/sftp/std/src/main.rs | 6 +-- sftp/src/asyncsftpserver.rs | 2 +- sftp/src/lib.rs | 51 +++++++++++++++------- sftp/src/opaquefilehandle.rs | 2 +- sftp/src/proto.rs | 22 ++++++---- sftp/src/requestholder.rs | 2 - sftp/src/sftperror.rs | 5 ++- sftp/src/sftphandler.rs | 12 +++-- sftp/src/sftpserver.rs | 8 ++-- sftp/src/sftpsource.rs | 4 +- 13 files changed, 77 insertions(+), 52 deletions(-) diff --git a/demo/sftp/std/src/demofilehandlemanager.rs b/demo/sftp/std/src/demofilehandlemanager.rs index e7bae95a..feaa126f 100644 --- a/demo/sftp/std/src/demofilehandlemanager.rs +++ b/demo/sftp/std/src/demofilehandlemanager.rs @@ -1,6 +1,5 @@ -use sunset_sftp::{ - OpaqueFileHandle, OpaqueFileHandleManager, PathFinder, StatusCode, -}; +use sunset_sftp::handles::{OpaqueFileHandle, OpaqueFileHandleManager, PathFinder}; +use sunset_sftp::protocol::StatusCode; use std::collections::HashMap; // Not enforced. Only for std. For no_std environments other solutions can be used to store Key, Value diff --git a/demo/sftp/std/src/demoopaquefilehandle.rs b/demo/sftp/std/src/demoopaquefilehandle.rs index b7a1b92c..67c2fc6b 100644 --- a/demo/sftp/std/src/demoopaquefilehandle.rs +++ b/demo/sftp/std/src/demoopaquefilehandle.rs @@ -1,4 +1,5 @@ -use sunset_sftp::{FileHandle, OpaqueFileHandle}; +use sunset_sftp::handles::OpaqueFileHandle; +use sunset_sftp::protocol::FileHandle; use sunset::sshwire::{BinString, WireError}; diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 79591175..8ad893c5 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -3,10 +3,9 @@ use crate::{ demoopaquefilehandle::DemoOpaqueFileHandle, }; -use sunset_sftp::{ - Attrs, DirReply, Filename, Name, NameEntry, OpaqueFileHandleManager, PathFinder, - ReadReply, SftpOpResult, SftpServer, StatusCode, -}; +use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; +use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; +use sunset_sftp::server::{DirReply, ReadReply, SftpOpResult, SftpServer}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 74403ad6..51d4f24f 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -148,10 +148,10 @@ impl DemoServer for StdDemo { info!("SFTP loop has received a channel handle {:?}", ch.num()); - // TODO: Do some research to find reasonable default buffer lengths + // TODO Do some research to find reasonable default buffer lengths let mut buffer_in = [0u8; 512]; let mut buffer_out = [0u8; 384]; - let mut incomplete_request_buffer = [0u8; 128]; // TODO: Find a non arbitrary length + let mut incomplete_request_buffer = [0u8; 128]; // TODO Find a non arbitrary length match { let mut stdio = serv.stdio(ch).await?; @@ -209,7 +209,7 @@ impl DemoServer for StdDemo { } } -// TODO: pool_size should be NUM_LISTENERS but needs a literal +// TODO pool_size should be NUM_LISTENERS but needs a literal #[embassy_executor::task(pool_size = 4)] async fn listen( stack: Stack<'static>, diff --git a/sftp/src/asyncsftpserver.rs b/sftp/src/asyncsftpserver.rs index b2bd93e8..33458479 100644 --- a/sftp/src/asyncsftpserver.rs +++ b/sftp/src/asyncsftpserver.rs @@ -80,7 +80,7 @@ impl<'g, 'a> DirReply<'g, 'a> { pub async fn reply(self, data: &[u8]) {} } -// TODO: Implement correct Channel Out +// TODO Implement correct Channel Out pub struct ChanOut<'g, 'a> { _phantom_g: PhantomData<&'g ()>, _phantom_a: PhantomData<&'a ()>, diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 1ed6689e..2fc6465b 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,31 +1,52 @@ +//! SFTP (SSH File Transfer Protocol) implementation extending [`sunset`]. +//! +//! Partially Implements SFTP v3 as defined in [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02). +//! +//! **Work in Progress**: Currently focuses on file upload operations. +//! Long packets for other request types and additional SFTP operations are not yet implemented. +//! `no_std` compatibility is intended but not yet complete. +//! +//! See example usage in the `demo/sftd/std` directory. + #![forbid(unsafe_code)] #![warn(missing_docs)] mod opaquefilehandle; mod proto; mod requestholder; +mod sftperror; mod sftphandler; mod sftpserver; mod sftpsink; mod sftpsource; -mod sftperror; +pub use sftphandler::SftpHandler; -pub use sftpserver::DirReply; -pub use sftpserver::ReadReply; -pub use sftpserver::SftpOpResult; -pub use sftpserver::SftpServer; +pub mod server { -pub use sftphandler::SftpHandler; + pub use crate::sftpserver::DirReply; + pub use crate::sftpserver::ReadReply; + pub use crate::sftpserver::SftpOpResult; + pub use crate::sftpserver::SftpServer; +} -pub use opaquefilehandle::{OpaqueFileHandle, OpaqueFileHandleManager, PathFinder}; +pub mod handles { + pub use crate::opaquefilehandle::OpaqueFileHandle; + pub use crate::opaquefilehandle::OpaqueFileHandleManager; + pub use crate::opaquefilehandle::PathFinder; +} -pub use sftperror::{SftpError, SftpResult}; +pub mod protocol { + pub use crate::proto::Attrs; + pub use crate::proto::FileHandle; + pub use crate::proto::Filename; + pub use crate::proto::Name; + pub use crate::proto::NameEntry; + pub use crate::proto::PathInfo; + pub use crate::proto::StatusCode; +} -pub use proto::Attrs; -pub use proto::FileHandle; -pub use proto::Filename; -pub use proto::Name; -pub use proto::NameEntry; -pub use proto::PathInfo; -pub use proto::StatusCode; +pub mod error { + pub use crate::sftperror::SftpError; + pub use crate::sftperror::SftpResult; +} diff --git a/sftp/src/opaquefilehandle.rs b/sftp/src/opaquefilehandle.rs index d96114a6..18f0662b 100644 --- a/sftp/src/opaquefilehandle.rs +++ b/sftp/src/opaquefilehandle.rs @@ -1,4 +1,4 @@ -use crate::FileHandle; +use crate::protocol::FileHandle; use sunset::sshwire::WireResult; diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index bb04baf0..cd606478 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -47,7 +47,7 @@ impl<'a> From<&'a str> for Filename<'a> { } } -// TODO: standardize the encoding of filenames as str +// TODO standardize the encoding of filenames as str impl<'a> Filename<'a> { /// pub fn as_str(&self) -> Result<&'a str, WireError> { @@ -176,7 +176,7 @@ pub struct NameEntry<'a> { pub attrs: Attrs, } -// TODO: Will a Vector be an issue for no_std? +// TODO Will a Vector be an issue for no_std? // Maybe we should migrate this to heapless::Vec and let the user decide // the number of elements via features flags? /// A collection of [`NameEntry`] used for [ssh_fxp_name responses](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7). @@ -327,7 +327,7 @@ impl Attrs { if self.atime.is_some() || self.mtime.is_some() { flags += AttrsFlags::SSH_FILEXFER_ATTR_ACMODTIME } - // TODO: Implement extensions + // TODO Implement extensions // if self.ext_count.is_some() { // flags += AttrsFlags::SSH_FILEXFER_ATTR_EXTENDED // } @@ -359,7 +359,7 @@ impl SSHEncode for Attrs { if let Some(value) = self.mtime.as_ref() { value.enc(s)? } - // TODO: Implement extensions + // TODO Implement extensions // if let Some(value) = self.ext_count.as_ref() { value.enc(s)? } Ok(()) @@ -387,7 +387,7 @@ impl<'de> SSHDecode<'de> for Attrs { attrs.atime = Some(u32::dec(s)?); attrs.mtime = Some(u32::dec(s)?); } - // TODO: Implement extensions + // TODO Implement extensions // if flags & AttrsFlags::SSH_FILEXFER_ATTR_EXTENDED != 0{ Ok(attrs) @@ -618,6 +618,8 @@ macro_rules! sftpmessages { } } + // TODO Maybe change WireResult -> SftpResult and SSHSink to SftpSink? + // This way I have more internal details and can return a Error::bug() if required /// Encode a request. /// /// Used by a SFTP client. Does not include the length field. @@ -625,7 +627,7 @@ macro_rules! sftpmessages { if !self.sftp_num().is_request() { return Err(WireError::PacketWrong) // return Err(Error::bug()) - // TODO: I understand that it would be a bad call of encode_response and + // I understand that it would be a bad call of encode_response and // therefore a bug, bug Error::bug() is not compatible with WireResult } @@ -637,6 +639,8 @@ macro_rules! sftpmessages { self.enc(s) } + // TODO Maybe change WireResult -> SftpResult and SSHSource to SftpSource? + // This way I have more internal details and can return a more appropriate error if required /// Decode a response. /// /// Used by a SFTP client. Does not include the length field. @@ -651,7 +655,7 @@ macro_rules! sftpmessages { if !num.is_response() { return Err(WireError::PacketWrong) // return error::SSHProto.fail(); - // TODO: Not an error in the SSHProtocol rather the SFTP Protocol. + // Not an error in the SSHProtocol rather the SFTP Protocol. } let id = ReqId(u32::dec(s)?); @@ -690,6 +694,8 @@ macro_rules! sftpmessages { } } + // TODO Maybe change WireResult -> SftpResult and SSHSink to SftpSink? + // This way I have more internal details and can return a Error::bug() if required /// Encode a response. /// /// Used by a SFTP server. Does not include the length field. @@ -700,7 +706,7 @@ macro_rules! sftpmessages { if !self.sftp_num().is_response() { return Err(WireError::PacketWrong) // return Err(Error::bug()) - // TODO: I understand that it would be a bad call of encode_response and + // I understand that it would be a bad call of encode_response and // therefore a bug, bug Error::bug() is not compatible with WireResult } diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs index ed0abf4e..e1427fc2 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/requestholder.rs @@ -212,7 +212,6 @@ impl<'a> RequestHolder<'a> { self.buffer_fill_index - proto::SFTP_FIELD_LEN_LENGTH + remaining_packet_len ); - // TODO: Fix the mess with the logic and the indexes to address the slice. IT IS PANICKING if remaining_packet_len <= self.remaining_len() { // We have all the remaining packet bytes in the slice and fits in the buffer @@ -321,7 +320,6 @@ impl<'a> RequestHolder<'a> { /// Returns the number of bytes unused at the end of the buffer, /// this is, the remaining length fn remaining_len(&self) -> usize { - // self.buffer.len() - self.buffer_fill_index - 1 // TODO: Off by one? self.buffer.len() - self.buffer_fill_index } } diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index 13d6c333..134e716c 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -1,12 +1,13 @@ -use crate::{StatusCode, requestholder::RequestHolderError}; +use crate::protocol::StatusCode; +use crate::requestholder::RequestHolderError; use sunset::Error as SunsetError; use sunset::sshwire::WireError; use core::convert::From; use log::warn; -// TODO: Use it more broadly where reasonable +// TODO Use it more broadly where reasonable /// Errors that are specific to this SFTP lib #[derive(Debug)] pub enum SftpError { diff --git a/sftp/src/sftphandler.rs b/sftp/src/sftphandler.rs index 3c530a55..75e45723 100644 --- a/sftp/src/sftphandler.rs +++ b/sftp/src/sftphandler.rs @@ -1,3 +1,5 @@ +use crate::error::SftpError; +use crate::handles::OpaqueFileHandle; use crate::proto::{ self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, SftpPacket, Status, StatusCode, @@ -7,7 +9,6 @@ use crate::sftperror::SftpResult; use crate::sftpserver::SftpServer; use crate::sftpsink::SftpSink; use crate::sftpsource::SftpSource; -use crate::{OpaqueFileHandle, SftpError}; use sunset::Error as SunsetError; use sunset::sshwire::{SSHSource, WireError, WireResult}; @@ -39,7 +40,7 @@ enum FragmentedRequestState { ProcessingLongRequest, } -// TODO: Generalize this to allow other request types +// TODO Generalize this to allow other request types /// Used to keep record of a long SFTP Write request that does not fit in /// receiving buffer and requires processing in batches #[derive(Debug)] @@ -166,7 +167,6 @@ where if let Err(e) = self .incomplete_request_holder .try_append_for_valid_request( - // TODO: All your problems are here. Focus &buffer_in[buffer_in_remaining_index..], ) { @@ -293,8 +293,7 @@ where let usable_data = source .remaining() - .min(write_tracker.remain_data_len as usize); // TODO: Where does in_len comes from? - // in_len.min(write_tracker.remain_data_len as usize); + .min(write_tracker.remain_data_len as usize); let data_segment = // Fails!! source.dec_as_binstring(usable_data)?; @@ -546,7 +545,7 @@ where Ok(()) } - // TODO: Handle more long requests + // TODO Handle more long requests /// Handles long request that do not fit in the buffers and stores a tracker /// /// **WARNING:** Only `SSH_FXP_WRITE` has been implemented! @@ -593,7 +592,6 @@ where "Storing a write tracker for a fragmented write request" ); return Ok(write_tracker); - // partial_write_request_tracker = Some(write_tracker); // TODO: This might belong to return value } Err(e) => { error!("SFTP write thrown: {:?}", e); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 22f5bffb..38441404 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,5 +1,5 @@ use crate::{ - OpaqueFileHandle, + handles::OpaqueFileHandle, proto::{Attrs, Name, StatusCode}, }; @@ -88,7 +88,7 @@ where } } -// TODO: Define this +// TODO Define this pub struct ReadReply<'g, 'a> { chan: ChanOut<'g, 'a>, } @@ -97,7 +97,7 @@ impl<'g, 'a> ReadReply<'g, 'a> { pub fn reply(self, _data: &[u8]) {} } -// TODO: Define this +// TODO Define this pub struct DirReply<'g, 'a> { chan: ChanOut<'g, 'a>, } @@ -106,7 +106,7 @@ impl<'g, 'a> DirReply<'g, 'a> { pub fn reply(self, _data: &[u8]) {} } -// TODO: Implement correct Channel Out +// TODO Implement correct Channel Out pub struct ChanOut<'g, 'a> { _phantom_g: PhantomData<&'g ()>, _phantom_a: PhantomData<&'a ()>, diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index b1f0a2c0..67b743e8 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -1,9 +1,11 @@ +use crate::error::{SftpError, SftpResult}; +use crate::handles::OpaqueFileHandle; use crate::proto::{ ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, SFTP_MINIMUM_PACKET_LEN, SFTP_WRITE_REQID_INDEX, SftpNum, }; +use crate::protocol::FileHandle; use crate::sftphandler::PartialWriteRequestTracker; -use crate::{FileHandle, OpaqueFileHandle, SftpError, SftpResult}; use sunset::sshwire::{BinString, SSHDecode, SSHSource, WireError, WireResult}; From 624aff34fcb23f584693aae5b52b6fe880ee51d6 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 17:43:05 +1000 Subject: [PATCH 125/393] Partial functionality and refactor completed In this commit we have a working server that can receive files of arbitrary size. This can be tested running demo/sftp/std/ and running `demo/sftp/std/testing/test_long_write_requests.sh` Significative documentation has been added. SftpHandler relies on a FSM to process fragmented and long packet. --- sftp/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 2deb566d..3d500f3b 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sunset-sftp" -version = "0.1.0" +version = "0.1.1" edition = "2024" [dependencies] From 8f97d056031f35ad85035d5ba18f2b141dc901ab Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 17:43:05 +1000 Subject: [PATCH 126/393] Partial functionality and refactor completed In this commit we have a working server that can receive files of arbitrary size. This can be tested running demo/sftp/std/ and running `demo/sftp/std/testing/test_long_write_requests.sh` Significative documentation has been added. SftpHandler relies on a FSM to process fragmented and long packet. --- Cargo.lock | 2 +- sftp/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17e2beb8..17936b90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2915,7 +2915,7 @@ dependencies = [ [[package]] name = "sunset-sftp" -version = "0.1.0" +version = "0.1.1" dependencies = [ "log", "num_enum 0.7.4", diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 2deb566d..3d500f3b 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sunset-sftp" -version = "0.1.0" +version = "0.1.1" edition = "2024" [dependencies] From 5915b4a840f380d19a9cac6f2097922a8fcc851f Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 6 Oct 2025 11:58:16 +1100 Subject: [PATCH 127/393] SFTP Roadmap added to docs The crate doc has been rendered and improved. I propose a roadmap for the development of teh SFTP crate --- demo/sftp/std/src/demosftpserver.rs | 1 + sftp/src/lib.rs | 57 ++++++++++++++++++++++++++--- sftp/src/sftperror.rs | 3 +- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 8ad893c5..8e763185 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -29,6 +29,7 @@ impl PathFinder for PrivateFileHandler { } } +/// A basic demo server. Used as a demo and to test SFTP functionality pub struct DemoSftpServer { base_path: String, handlers_manager: diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 2fc6465b..a319faaa 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,12 +1,51 @@ -//! SFTP (SSH File Transfer Protocol) implementation extending [`sunset`]. +//! SFTP (SSH File Transfer Protocol) implementation for [`sunset`]. //! -//! Partially Implements SFTP v3 as defined in [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02). +//! (Partially) Implements SFTP v3 as defined in [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02). //! //! **Work in Progress**: Currently focuses on file upload operations. -//! Long packets for other request types and additional SFTP operations are not yet implemented. -//! `no_std` compatibility is intended but not yet complete. +//! Long packets for requests other than writing and additional SFTP operations +//! are not yet implemented. `no_std` compatibility is intended but not +//! yet complete. Please see the roadmap and use this crate carefully. //! -//! See example usage in the `demo/sftd/std` directory. +//! This crate implements a handler that, given a [`sunset::ChanHandle`] +//! a `sunset_async::SSHServer` and some auxiliary buffers, +//! can dispatch SFTP packets to a struct implementing [`crate::sftpserver::SftpServer`] trait. +//! +//! See example usage in the `../demo/sftd/std` directory for the intended usage +//! of this library. +//! +//! # Roadmap +//! +//! The following list is an opinionated collection of the points that should be +//! completed to provide growing functionality. +//! +//! ## Basic features +//! +//! - [x] [SFTP Protocol Initialization](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-4) (Only SFTP V3 supported) +//! - [x] [Canonicalizing the Server-Side Path Name](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.11) support +//! - [x] [Open, close](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3) +//! and [write](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) +//! - [ ] File [read](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4), +//! - [ ] File [stats](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8) +//! - [ ] Directory [Browsing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7) +//! +//! ## Minimal features for convenient usability +//! +//! - [ ] [Removing files](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.5) +//! - [ ] [Renaming files](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.5) +//! - [ ] [Creating directories](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.6) +//! - [ ] [Removing directories](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.6) +//! +//! ## Extended features +//! +//! - [ ] [Append, create and truncate files](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3) +//! files +//! - [ ] [Reading](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8) +//! files attributes +//! - [ ] [Setting](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.9) files attributes +//! - [ ] [Dealing with Symbolic links](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.10) +//! - [ ] [Vendor Specific](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-8) +//! request and responses #![forbid(unsafe_code)] #![warn(missing_docs)] @@ -20,8 +59,13 @@ mod sftpserver; mod sftpsink; mod sftpsource; +/// Main calling point for the library provided that the user implements +/// a [`crate::sftpserver::SftpServer`]. +/// +/// Please see basic usage at `../demo/sftd/std` pub use sftphandler::SftpHandler; +/// Structures and types used to add the details for the target system pub mod server { pub use crate::sftpserver::DirReply; @@ -30,12 +74,14 @@ pub mod server { pub use crate::sftpserver::SftpServer; } +/// Handles and helpers used by the [`sftpserver::SftpServer`] trait implementer pub mod handles { pub use crate::opaquefilehandle::OpaqueFileHandle; pub use crate::opaquefilehandle::OpaqueFileHandleManager; pub use crate::opaquefilehandle::PathFinder; } +/// SFTP Protocol types and structures pub mod protocol { pub use crate::proto::Attrs; pub use crate::proto::FileHandle; @@ -46,6 +92,7 @@ pub mod protocol { pub use crate::proto::StatusCode; } +/// Errors and results used in this crate pub mod error { pub use crate::sftperror::SftpError; pub use crate::sftperror::SftpResult; diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index 134e716c..553d622e 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -27,7 +27,8 @@ pub enum SftpError { NotSupported, /// The [`crate::sftpserver::SftpServer`] failed doing an IO operation FileServerError(StatusCode), - /// A RequestHolder instance throw an error. See [`crate::requestholder::RequestHolderError`] + // A RequestHolder instance throw an error. See [`crate::requestholder::RequestHolderError`] + /// A RequestHolder instance threw an error. See `RequestHolderError` RequestHolderError(RequestHolderError), /// A variant containing a [`WireError`] WireError(WireError), From 5132650921e14471715c00251f189caddabce9fe Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 8 Oct 2025 17:38:37 +1100 Subject: [PATCH 128/393] [ci skip] Adding no_std, removing unnecessary use of String --- sftp/src/lib.rs | 1 + sftp/src/sftpserver.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index a319faaa..de45fec9 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -49,6 +49,7 @@ #![forbid(unsafe_code)] #![warn(missing_docs)] +#![no_std] mod opaquefilehandle; mod proto; diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 38441404..e66b3ac7 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -54,10 +54,10 @@ where buf: &[u8], ) -> SftpOpResult<()> { log::error!( - "SftpServer Write operation: handle = {:?}, offset = {:?}, buf = {:?}", + "SftpServer Write operation not defined: handle = {:?}, offset = {:?}, buf = {:?}", opaque_file_handle, offset, - String::from_utf8(buf.to_vec()) + buf ); Ok(()) } From 338cf827d9e75c8d50b31280dfa7549f716a3748 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 9 Oct 2025 13:53:16 +1100 Subject: [PATCH 129/393] [ci skip] WIP: Added OpenDir operation SftpServer trait and DemoSftpServer modified to allow the use of Directories --- demo/sftp/std/src/demosftpserver.rs | 227 ++++++++++++++++++++-------- sftp/src/proto.rs | 14 +- sftp/src/sftpserver.rs | 4 +- 3 files changed, 178 insertions(+), 67 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 8e763185..9d4044c0 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -9,35 +9,87 @@ use sunset_sftp::server::{DirReply, ReadReply, SftpOpResult, SftpServer}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -use std::{fs::File, os::unix::fs::FileExt}; +use std::fs; +use std::{fs::File, os::unix::fs::FileExt, path::Path}; -pub(crate) struct PrivateFileHandler { - file_path: String, +pub(crate) enum PrivatePathHandle { + File(PrivateFileHandle), + Directory(PrivateDirHandle), +} + +pub(crate) struct PrivateFileHandle { + path: String, permissions: Option, file: File, } +pub(crate) struct PrivateDirHandle { + path: String, +} + static OPAQUE_SALT: &'static str = "12d%32"; -impl PathFinder for PrivateFileHandler { - fn matches(&self, path: &PrivateFileHandler) -> bool { - self.file_path.as_str().eq_ignore_ascii_case(path.get_path_ref()) +impl PathFinder for PrivatePathHandle { + fn matches(&self, path: &Self) -> bool { + match self { + PrivatePathHandle::File(self_private_path_handler) => { + if let PrivatePathHandle::File(private_file_handle) = path { + return self_private_path_handler.matches(private_file_handle); + } else { + false + } + } + PrivatePathHandle::Directory(self_private_dir_handle) => { + if let PrivatePathHandle::Directory(private_dir_handle) = path { + self_private_dir_handle.matches(private_dir_handle) + } else { + false + } + } + } } fn get_path_ref(&self) -> &str { - self.file_path.as_str() + match self { + PrivatePathHandle::File(private_file_handler) => { + private_file_handler.get_path_ref() + } + PrivatePathHandle::Directory(private_dir_handle) => { + private_dir_handle.get_path_ref() + } + } + } +} + +impl PathFinder for PrivateFileHandle { + fn matches(&self, path: &PrivateFileHandle) -> bool { + self.path.as_str().eq_ignore_ascii_case(path.get_path_ref()) + } + + fn get_path_ref(&self) -> &str { + self.path.as_str() + } +} + +impl PathFinder for PrivateDirHandle { + fn matches(&self, path: &PrivateDirHandle) -> bool { + self.path.as_str().eq_ignore_ascii_case(path.get_path_ref()) + } + + fn get_path_ref(&self) -> &str { + self.path.as_str() } } /// A basic demo server. Used as a demo and to test SFTP functionality pub struct DemoSftpServer { base_path: String, - handlers_manager: - DemoFileHandleManager, + handlers_manager: DemoFileHandleManager, } impl DemoSftpServer { pub fn new(base_path: String) -> Self { + // TODO What if the base_path does not exist? Create it or Return error? DemoSftpServer { base_path, handlers_manager: DemoFileHandleManager::new() } } } @@ -48,41 +100,71 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { filename: &str, attrs: &Attrs, ) -> SftpOpResult { - debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); - - let poxit_attr = attrs - .permissions - .as_ref() - .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED)?; - let can_write = poxit_attr & 0o222 > 0; - let can_read = poxit_attr & 0o444 > 0; - debug!( - "File open for read/write access: can_read={:?}, can_write={:?}", - can_read, can_write - ); + let path = Path::new(filename); - let file = File::options() - .read(can_read) - .write(can_write) - .create(true) - .open(filename) - .map_err(|_| StatusCode::SSH_FX_FAILURE)?; - - let fh = self.handlers_manager.insert( - PrivateFileHandler { - file_path: filename.into(), - permissions: attrs.permissions, - file, - }, - OPAQUE_SALT, - ); + let metadata = fs::symlink_metadata(path).map_err(|e| { + warn!("Could not open {:?}: {:?}", filename, e); + StatusCode::SSH_FX_NO_SUCH_FILE + })?; - debug!( - "Filename \"{:?}\" will have the obscured file handle: {:?}", - filename, fh - ); + if metadata.is_symlink() { + return Err(StatusCode::SSH_FX_OP_UNSUPPORTED); + } + + if metadata.is_dir() { + debug!("Open Directory = {:?}", filename); - fh + let fh = self.handlers_manager.insert( + PrivatePathHandle::Directory(PrivateDirHandle { + path: filename.into(), + }), + OPAQUE_SALT, + ); + + debug!( + "Directory \"{:?}\" will have the obscured file handle: {:?}", + filename, fh + ); + + // Err(StatusCode::SSH_FX_BAD_MESSAGE) + fh + } else { + debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); + + let poxit_attr = attrs + .permissions + .as_ref() + .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED)?; + let can_write = poxit_attr & 0o222 > 0; + let can_read = poxit_attr & 0o444 > 0; + debug!( + "File open for read/write access: can_read={:?}, can_write={:?}", + can_read, can_write + ); + + let file = File::options() + .read(can_read) + .write(can_write) + .create(true) + .open(filename) + .map_err(|_| StatusCode::SSH_FX_FAILURE)?; + + let fh = self.handlers_manager.insert( + PrivatePathHandle::File(PrivateFileHandle { + path: filename.into(), + permissions: attrs.permissions, + file, + }), + OPAQUE_SALT, + ); + + debug!( + "Filename \"{:?}\" will have the obscured file handle: {:?}", + filename, fh + ); + + fh + } } fn realpath(&mut self, dir: &str) -> SftpOpResult> { @@ -107,12 +189,24 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { opaque_file_handle: &DemoOpaqueFileHandle, ) -> SftpOpResult<()> { if let Some(handle) = self.handlers_manager.remove(opaque_file_handle) { - debug!( - "SftpServer Close operation on {:?} was successful", - handle.file_path - ); - drop(handle.file); // Not really required but illustrative - Ok(()) + match handle { + PrivatePathHandle::File(private_file_handle) => { + debug!( + "SftpServer Close operation on file {:?} was successful", + private_file_handle.path + ); + drop(private_file_handle.file); // Not really required but illustrative + Ok(()) + } + PrivatePathHandle::Directory(private_dir_handle) => { + debug!( + "SftpServer Close operation on dir {:?} was successful", + private_dir_handle.path + ); + + Ok(()) + } + } } else { error!( "SftpServer Close operation on handle {:?} failed", @@ -128,41 +222,44 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { offset: u64, buf: &[u8], ) -> SftpOpResult<()> { - let private_file_handle = self + if let PrivatePathHandle::File(private_file_handle) = self .handlers_manager .get_private_as_ref(opaque_file_handle) - .ok_or(StatusCode::SSH_FX_FAILURE)?; - - let permissions_poxit = (private_file_handle - .permissions - .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED))?; + .ok_or(StatusCode::SSH_FX_FAILURE)? + { + let permissions_poxit = (private_file_handle + .permissions + .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED))?; - if (permissions_poxit & 0o222) == 0 { - return Err(StatusCode::SSH_FX_PERMISSION_DENIED); - }; + if (permissions_poxit & 0o222) == 0 { + return Err(StatusCode::SSH_FX_PERMISSION_DENIED); + }; - log::trace!( + log::trace!( "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buf = {:?}", opaque_file_handle, - private_file_handle.file_path, + private_file_handle.path, offset, String::from_utf8(buf.to_vec()) ); - let bytes_written = private_file_handle - .file - .write_at(buf, offset) - .map_err(|_| StatusCode::SSH_FX_FAILURE)?; + let bytes_written = private_file_handle + .file + .write_at(buf, offset) + .map_err(|_| StatusCode::SSH_FX_FAILURE)?; - log::debug!( + log::debug!( "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buffer length = {:?}, bytes written = {:?}", opaque_file_handle, - private_file_handle.file_path, + private_file_handle.path, offset, buf.len(), bytes_written ); - Ok(()) + Ok(()) + } else { + Err(StatusCode::SSH_FX_PERMISSION_DENIED) + } } fn read( diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index cd606478..1ded97ad 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -60,6 +60,8 @@ impl<'a> Filename<'a> { #[derive(Debug, Clone, Copy, PartialEq, Eq, SSHEncode, SSHDecode)] pub struct FileHandle<'a>(pub BinString<'a>); +// ========================== Initialization =========================== + /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 pub const SFTP_VERSION: u32 = 3; @@ -79,6 +81,8 @@ pub struct InitVersionLowest { // TODO variable number of ExtPair } +// ============================= Requests ============================== + /// Used for `ssh_fxp_open` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Open<'a> { @@ -90,6 +94,13 @@ pub struct Open<'a> { pub attrs: Attrs, } +/// Used for `ssh_fxp_open` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3). +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct OpenDir<'a> { + /// The relative or absolute path of the directory to be open + pub filename: Filename<'a>, +} + /// Used for `ssh_fxp_close` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Close<'a> { @@ -122,7 +133,7 @@ pub struct Write<'a> { pub data: BinString<'a>, } -// Responses +// ============================= Responses ============================= /// Used for `ssh_fxp_realpath` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.11). #[derive(Debug, SSHEncode, SSHDecode)] @@ -757,6 +768,7 @@ sftpmessages! [ (4, Close, Close<'a>, "ssh_fxp_close"), (5, Read, Read<'a>, "ssh_fxp_read"), (6, Write, Write<'a>, "ssh_fxp_write"), + (11, OpenDir, OpenDir<'a>, "ssh_fxp_opendir"), (16, PathInfo, PathInfo<'a>, "ssh_fxp_realpath"), }, diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index e66b3ac7..a664a280 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -17,7 +17,9 @@ where T: OpaqueFileHandle, { /// Opens a file or directory for reading/writing - fn open(&'_ mut self, filename: &str, attrs: &Attrs) -> SftpOpResult { + /// + /// In the case of a path the attrs parameter content is ignored + fn open(&'_ mut self, path: &str, attrs: &Attrs) -> SftpOpResult { log::error!( "SftpServer Open operation not defined: filename = {:?}, attrs = {:?}", filename, From c0e0fcac146e1a9ceeed66b37414c8bf221e21bd Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 9 Oct 2025 16:41:13 +1100 Subject: [PATCH 130/393] [skip ci] WIP: Added OpenDir and ReadDir requests I am at the point where I can start implementing the way I am going to put all the data in the channel. Not trivial --- demo/sftp/std/src/demosftpserver.rs | 246 +++++++++++++++---------- demo/sftp/std/testing/test_read_dir.sh | 30 +++ sftp/Cargo.toml | 3 + sftp/src/proto.rs | 13 +- sftp/src/sftperror.rs | 3 + sftp/src/sftphandler.rs | 44 ++++- sftp/src/sftpserver.rs | 16 +- 7 files changed, 240 insertions(+), 115 deletions(-) create mode 100755 demo/sftp/std/testing/test_read_dir.sh diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 9d4044c0..33272ef5 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -5,24 +5,30 @@ use crate::{ use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; -use sunset_sftp::server::{DirReply, ReadReply, SftpOpResult, SftpServer}; +use sunset_sftp::server::{ReadReply, SftpOpResult, SftpServer}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; use std::fs; +use std::os::linux::fs::MetadataExt; +use std::os::unix::fs::PermissionsExt; +use std::time::SystemTime; use std::{fs::File, os::unix::fs::FileExt, path::Path}; +#[derive(Debug)] pub(crate) enum PrivatePathHandle { File(PrivateFileHandle), Directory(PrivateDirHandle), } +#[derive(Debug)] pub(crate) struct PrivateFileHandle { path: String, permissions: Option, file: File, } +#[derive(Debug)] pub(crate) struct PrivateDirHandle { path: String, } @@ -84,13 +90,13 @@ impl PathFinder for PrivateDirHandle { /// A basic demo server. Used as a demo and to test SFTP functionality pub struct DemoSftpServer { base_path: String, - handlers_manager: DemoFileHandleManager, + handles_manager: DemoFileHandleManager, } impl DemoSftpServer { pub fn new(base_path: String) -> Self { // TODO What if the base_path does not exist? Create it or Return error? - DemoSftpServer { base_path, handlers_manager: DemoFileHandleManager::new() } + DemoSftpServer { base_path, handles_manager: DemoFileHandleManager::new() } } } @@ -100,71 +106,57 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { filename: &str, attrs: &Attrs, ) -> SftpOpResult { - let path = Path::new(filename); - - let metadata = fs::symlink_metadata(path).map_err(|e| { - warn!("Could not open {:?}: {:?}", filename, e); - StatusCode::SSH_FX_NO_SUCH_FILE - })?; - - if metadata.is_symlink() { - return Err(StatusCode::SSH_FX_OP_UNSUPPORTED); - } - - if metadata.is_dir() { - debug!("Open Directory = {:?}", filename); - - let fh = self.handlers_manager.insert( - PrivatePathHandle::Directory(PrivateDirHandle { - path: filename.into(), - }), - OPAQUE_SALT, - ); + debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); + + let poxit_attr = attrs + .permissions + .as_ref() + .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED)?; + let can_write = poxit_attr & 0o222 > 0; + let can_read = poxit_attr & 0o444 > 0; + debug!( + "File open for read/write access: can_read={:?}, can_write={:?}", + can_read, can_write + ); - debug!( - "Directory \"{:?}\" will have the obscured file handle: {:?}", - filename, fh - ); + let file = File::options() + .read(can_read) + .write(can_write) + .create(true) + .open(filename) + .map_err(|_| StatusCode::SSH_FX_FAILURE)?; + + let fh = self.handles_manager.insert( + PrivatePathHandle::File(PrivateFileHandle { + path: filename.into(), + permissions: attrs.permissions, + file, + }), + OPAQUE_SALT, + ); - // Err(StatusCode::SSH_FX_BAD_MESSAGE) - fh - } else { - debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); + debug!( + "Filename \"{:?}\" will have the obscured file handle: {:?}", + filename, fh + ); - let poxit_attr = attrs - .permissions - .as_ref() - .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED)?; - let can_write = poxit_attr & 0o222 > 0; - let can_read = poxit_attr & 0o444 > 0; - debug!( - "File open for read/write access: can_read={:?}, can_write={:?}", - can_read, can_write - ); + fh + } - let file = File::options() - .read(can_read) - .write(can_write) - .create(true) - .open(filename) - .map_err(|_| StatusCode::SSH_FX_FAILURE)?; + fn opendir(&mut self, dir: &str) -> SftpOpResult { + debug!("Open Directory = {:?}", dir); - let fh = self.handlers_manager.insert( - PrivatePathHandle::File(PrivateFileHandle { - path: filename.into(), - permissions: attrs.permissions, - file, - }), - OPAQUE_SALT, - ); + let dir_handle = self.handles_manager.insert( + PrivatePathHandle::Directory(PrivateDirHandle { path: dir.into() }), + OPAQUE_SALT, + ); - debug!( - "Filename \"{:?}\" will have the obscured file handle: {:?}", - filename, fh - ); + debug!( + "Directory \"{:?}\" will have the obscured file handle: {:?}", + dir, dir_handle + ); - fh - } + dir_handle } fn realpath(&mut self, dir: &str) -> SftpOpResult> { @@ -188,7 +180,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { &mut self, opaque_file_handle: &DemoOpaqueFileHandle, ) -> SftpOpResult<()> { - if let Some(handle) = self.handlers_manager.remove(opaque_file_handle) { + if let Some(handle) = self.handles_manager.remove(opaque_file_handle) { match handle { PrivatePathHandle::File(private_file_handle) => { debug!( @@ -216,6 +208,20 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } } + fn read( + &mut self, + opaque_file_handle: &DemoOpaqueFileHandle, + offset: u64, + _reply: &mut ReadReply<'_, '_>, + ) -> SftpOpResult<()> { + log::error!( + "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", + opaque_file_handle, + offset + ); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + fn write( &mut self, opaque_file_handle: &DemoOpaqueFileHandle, @@ -223,7 +229,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { buf: &[u8], ) -> SftpOpResult<()> { if let PrivatePathHandle::File(private_file_handle) = self - .handlers_manager + .handles_manager .get_private_as_ref(opaque_file_handle) .ok_or(StatusCode::SSH_FX_FAILURE)? { @@ -236,12 +242,12 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { }; log::trace!( - "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buf = {:?}", - opaque_file_handle, - private_file_handle.path, - offset, - String::from_utf8(buf.to_vec()) - ); + "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buf = {:?}", + opaque_file_handle, + private_file_handle.path, + offset, + String::from_utf8(buf.to_vec()) + ); let bytes_written = private_file_handle .file .write_at(buf, offset) @@ -249,12 +255,12 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { log::debug!( "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buffer length = {:?}, bytes written = {:?}", - opaque_file_handle, - private_file_handle.path, - offset, - buf.len(), - bytes_written - ); + opaque_file_handle, + private_file_handle.path, + offset, + buf.len(), + bytes_written + ); Ok(()) } else { @@ -262,34 +268,74 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } } - fn read( - &mut self, - opaque_file_handle: &DemoOpaqueFileHandle, - offset: u64, - _reply: &mut ReadReply<'_, '_>, - ) -> SftpOpResult<()> { - log::error!( - "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", - opaque_file_handle, - offset - ); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) - } - - fn opendir(&mut self, dir: &str) -> SftpOpResult { - log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) - } - fn readdir( &mut self, - opaque_file_handle: &DemoOpaqueFileHandle, - _reply: &mut DirReply<'_, '_>, + opaque_dir_handle: &DemoOpaqueFileHandle, + // _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { - log::error!( - "SftpServer ReadDir operation not defined: handle = {:?}", - opaque_file_handle - ); + debug!("read dir for {:?}", opaque_dir_handle); + if let PrivatePathHandle::Directory(dir) = self + .handles_manager + .get_private_as_ref(opaque_dir_handle) + .ok_or(StatusCode::SSH_FX_FAILURE)? + { + let path_str = dir.path.clone(); + debug!("opaque handle found in handles manager: {:?}", path_str); + let dir_path = Path::new(&path_str); + debug!("path: {:?}", dir_path); + if dir_path.is_dir() { + debug!("SftpServer ReadDir operation path = {:?}", dir_path); + + let dir_iterator = fs::read_dir(dir_path).map_err(|err| { + error!("could not get the directory {:?}: {:?}", path_str, err); + StatusCode::SSH_FX_PERMISSION_DENIED + })?; + + debug!("got iterator = {:?}", dir_iterator); + + for entry in dir_iterator { + if let Ok(entry) = entry { + // info!("{:?}", entry); + if let Ok(metadata) = entry.metadata() { + if metadata.accessed().is_ok() + && metadata.modified().is_ok() + { + info!( + "{:?} : size = {:?}, uid = {:?}, gid = {:?}, \ + permissions = {:?}, atime = {:?}, mtime = {:?}, \ + ", + entry.path(), + metadata.len(), + metadata.st_uid(), + metadata.st_uid(), + metadata.permissions().mode(), + metadata + .accessed() + .unwrap() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(), + metadata + .modified() + .unwrap() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(), + ); + } + } + } + } + } else { + error!("the path is not a directory = {:?}", dir_path); + return Err(StatusCode::SSH_FX_NO_SUCH_FILE); + } + } else { + error!("Could not find the directory for {:?}", opaque_dir_handle); + return Err(StatusCode::SSH_FX_NO_SUCH_FILE); + } + + error!("What is the return that we are looking for?"); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } } diff --git a/demo/sftp/std/testing/test_read_dir.sh b/demo/sftp/std/testing/test_read_dir.sh new file mode 100755 index 00000000..75d0dcb3 --- /dev/null +++ b/demo/sftp/std/testing/test_read_dir.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Define test files +FILES=("A_random" "B_random" "D_random" "E_random" "F_random" "G_random") + +# Generate random data files +echo "Generating random data files..." +dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null + +# Generating copies of the test file +echo "Creating copies for each test file..." +for file in "${FILES[@]}"; do + cp ./512B_random "./${file}" + echo "Created: ${file}" +done +ls + +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." + +# Upload all files +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +$(printf 'put ./%s\n' "${FILES[@]}") +ls +bye +EOF + diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 3d500f3b..9a591f9f 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -5,8 +5,11 @@ edition = "2024" [dependencies] sunset = { version = "0.3.0", path = "../" } +sunset-async = { path = "../async", version = "0.3" } sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } + +embedded-io-async = "0.6" num_enum = {version = "0.7.4"} paste = "1.0" log = "0.4" diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 1ded97ad..de466721 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -94,11 +94,11 @@ pub struct Open<'a> { pub attrs: Attrs, } -/// Used for `ssh_fxp_open` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3). +/// Used for `ssh_fxp_open` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7). #[derive(Debug, SSHEncode, SSHDecode)] pub struct OpenDir<'a> { /// The relative or absolute path of the directory to be open - pub filename: Filename<'a>, + pub dirname: Filename<'a>, } /// Used for `ssh_fxp_close` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3). @@ -121,6 +121,14 @@ pub struct Read<'a> { pub len: u32, } +/// Used for `ssh_fxp_readdir` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7). +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct ReadDir<'a> { + /// An opaque handle that is used by the server to identify an open + /// file or folder. + pub handle: FileHandle<'a>, +} + /// Used for `ssh_fxp_write` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Write<'a> { @@ -769,6 +777,7 @@ sftpmessages! [ (5, Read, Read<'a>, "ssh_fxp_read"), (6, Write, Write<'a>, "ssh_fxp_write"), (11, OpenDir, OpenDir<'a>, "ssh_fxp_opendir"), + (12, ReadDir, ReadDir<'a>, "ssh_fxp_readdir"), (16, PathInfo, PathInfo<'a>, "ssh_fxp_realpath"), }, diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index 553d622e..cd01f748 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -25,6 +25,8 @@ pub enum SftpError { /// - The request has not been handled by an [`crate::sftpserver::SftpServer`] /// - Long request which its handling was not implemented NotSupported, + /// The connection has been closed by the client + ClientDisconnected, /// The [`crate::sftpserver::SftpServer`] failed doing an IO operation FileServerError(StatusCode), // A RequestHolder instance throw an error. See [`crate::requestholder::RequestHolderError`] @@ -98,6 +100,7 @@ impl From for SunsetError { warn!("Casting error loosing information: {:?}", value); SunsetError::Bug } + SftpError::ClientDisconnected => SunsetError::ChannelEOF, } } } diff --git a/sftp/src/sftphandler.rs b/sftp/src/sftphandler.rs index 75e45723..925d7c64 100644 --- a/sftp/src/sftphandler.rs +++ b/sftp/src/sftphandler.rs @@ -12,8 +12,10 @@ use crate::sftpsource::SftpSource; use sunset::Error as SunsetError; use sunset::sshwire::{SSHSource, WireError, WireResult}; +use sunset_async::ChanInOut; -use core::{u32, usize}; +use core::u32; +use embedded_io_async::{Read, Write}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; @@ -135,7 +137,6 @@ where pub async fn process( &mut self, buffer_in: &[u8], - // incomplete_request_holder: &mut RequestHolder<'_>, buffer_out: &mut [u8], ) -> SftpResult { let in_len = buffer_in.len(); @@ -537,9 +538,44 @@ where } } } + SftpPacket::OpenDir(req_id, open_dir) => { + match file_server.opendir(open_dir.dirname.as_str()?) { + Ok(opaque_file_handle) => { + let response = SftpPacket::Handle( + req_id, + proto::Handle { + handle: opaque_file_handle.into_file_handle(), + }, + ); + response.encode_response(sink)?; + info!("Sending '{:?}'", response); + } + Err(status_code) => { + error!("Open failed: {:?}", status_code); + push_general_failure(req_id, "", sink)?; + } + }; + } + SftpPacket::ReadDir(req_id, read_dir) => { + // TODO Implement the mechanism you are going to use to + // handle the list of elements + match file_server.readdir(&T::try_from(&read_dir.handle)?) { + Ok(_) => { + todo!("Dance starts here"); + } + Err(status_code) => { + error!("Open failed: {:?}", status_code); + push_unsupported(req_id, sink)?; + } + }; + error!("Unsupported Read Dir : {:?}", read_dir); + // return Err(SftpError::NotSupported); + // push_unsupported(ReqId(0), sink)?; + } _ => { - error!("Unsuported request type"); - push_unsupported(ReqId(0), sink)?; + error!("Unsuported request type: {:?}", request); + return Err(SftpError::NotSupported); + // push_unsupported(ReqId(0), sink)?; } } Ok(()) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index a664a280..05cdfc22 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -16,13 +16,11 @@ pub trait SftpServer<'a, T> where T: OpaqueFileHandle, { - /// Opens a file or directory for reading/writing - /// - /// In the case of a path the attrs parameter content is ignored + /// Opens a file for reading/writing fn open(&'_ mut self, path: &str, attrs: &Attrs) -> SftpOpResult { log::error!( - "SftpServer Open operation not defined: filename = {:?}, attrs = {:?}", - filename, + "SftpServer Open operation not defined: path = {:?}, attrs = {:?}", + path, attrs ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -64,7 +62,7 @@ where Ok(()) } - /// Opens a directory + /// Opens a directory and returns a handle fn opendir(&mut self, dir: &str) -> SftpOpResult { log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -73,12 +71,12 @@ where /// Reads the list of items in a directory fn readdir( &mut self, - opaque_file_handle: &T, - _reply: &mut DirReply<'_, '_>, + opaque_dir_handle: &T, + // _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", - opaque_file_handle + opaque_dir_handle ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } From 61574b39830b5e88347d03f4c68d4d614afdba40 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 10 Oct 2025 16:05:06 +1100 Subject: [PATCH 131/393] [skip ci] WIP: moving main loop inside process_loop to have stdio in scope --- demo/sftp/std/src/main.rs | 18 +++--------------- sftp/src/sftphandler.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 51d4f24f..74fccb3a 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -164,22 +164,10 @@ impl DemoServer for StdDemo { &mut file_server, &mut incomplete_request_buffer, ); - loop { - let lr = stdio.read(&mut buffer_in).await?; - trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); - if lr == 0 { - debug!("client disconnected"); - break; - } + sftp_handler + .process_loop(&mut stdio, &mut buffer_in, &mut buffer_out) + .await?; - let lw = sftp_handler - .process(&buffer_in[0..lr], &mut buffer_out) - .await?; - if lw > 0 { - stdio.write(&mut buffer_out[0..lw]).await?; - trace!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); - } - } Ok::<_, Error>(()) } { Ok(_) => { diff --git a/sftp/src/sftphandler.rs b/sftp/src/sftphandler.rs index 925d7c64..94b8d213 100644 --- a/sftp/src/sftphandler.rs +++ b/sftp/src/sftphandler.rs @@ -478,6 +478,38 @@ where Ok(used_out_accumulated_index) } + /// WIP: A loop that will process all the request from stdio until + /// an EOF is received + pub async fn process_loop<'c>( + &mut self, + stdio: &mut ChanInOut<'c>, + buffer_in: &mut [u8], + buffer_out: &mut [u8], + ) -> SftpResult<()> { + loop { + let lr = stdio.read(buffer_in).await?; + trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); + if lr == 0 { + debug!("client disconnected"); + return Err(SftpError::ClientDisconnected); + } + + let lw = self.process(&buffer_in[0..lr], buffer_out).await?; + if lw > 0 { + let wo = stdio.write(&mut buffer_out[0..lw]).await?; + if wo != lw { + error!("SFTP ----> Sent incomplete {} < {}", wo, lw); + todo!( + "Fix write incomplete condition: Repeat until \ + all is gone down the channel" + ); + } + trace!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); + } + } + Ok(()) + } + fn handle_general_request( file_server: &mut S, sink: &mut SftpSink<'_>, From 12d5927ef6bbe4a78dfba1c635e71bdd59687023 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 10 Oct 2025 16:05:42 +1100 Subject: [PATCH 132/393] [skip ci] WIP: commenting out the no_std restriction for now --- sftp/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index de45fec9..740072c7 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -49,7 +49,7 @@ #![forbid(unsafe_code)] #![warn(missing_docs)] -#![no_std] +// #![no_std] mod opaquefilehandle; mod proto; From 93cd0eec1c47531862893165a069adf9c93cb268 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 10 Oct 2025 16:06:51 +1100 Subject: [PATCH 133/393] [skip ci] WIP: I missed out adding items to Cargo.lock --- Cargo.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 17936b90..de7fac7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2917,10 +2917,12 @@ dependencies = [ name = "sunset-sftp" version = "0.1.1" dependencies = [ + "embedded-io-async", "log", "num_enum 0.7.4", "paste", "sunset", + "sunset-async", "sunset-sshwire-derive", ] From ae86957840e6238ddecece5c4b145ce4e0b9f571 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 10 Oct 2025 16:36:51 +1100 Subject: [PATCH 134/393] [skip ci] WIP: Exploring the iterator option [skip ci] WIP: Experimenting with SftpServer DirReply structure For now I am only checking borrowings [skip ci] WIP: SftpSink playload_slice() [skip ci] WIP: Adding DirReply Visitor to `ReadDir` and `DirEntriesResponseHelpers` to sftpserver.rs These will help me standardize other implementations. Also, I have proven that the Visitor pattern can be used to process each directory entry without borrowing or lifetime issues --- demo/sftp/std/src/demosftpserver.rs | 171 +++++++++++++++++++++------- sftp/src/lib.rs | 3 + sftp/src/sftphandler.rs | 12 +- sftp/src/sftpserver.rs | 70 +++++++++++- sftp/src/sftpsink.rs | 12 +- 5 files changed, 223 insertions(+), 45 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 33272ef5..e4688d2b 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -3,16 +3,21 @@ use crate::{ demoopaquefilehandle::DemoOpaqueFileHandle, }; +use sunset::sshwire::SSHEncode; use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; -use sunset_sftp::server::{ReadReply, SftpOpResult, SftpServer}; +use sunset_sftp::server::{ + DirEntriesResponseHelpers, DirReply, ReadReply, SftpOpResult, SftpServer, + SftpSink, +}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -use std::fs; +use std::fs::DirEntry; use std::os::linux::fs::MetadataExt; use std::os::unix::fs::PermissionsExt; use std::time::SystemTime; +use std::{fs, io}; use std::{fs::File, os::unix::fs::FileExt, path::Path}; #[derive(Debug)] @@ -271,9 +276,10 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { fn readdir( &mut self, opaque_dir_handle: &DemoOpaqueFileHandle, - // _reply: &mut DirReply<'_, '_>, + visitor: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { debug!("read dir for {:?}", opaque_dir_handle); + if let PrivatePathHandle::Directory(dir) = self .handles_manager .get_private_as_ref(opaque_dir_handle) @@ -283,6 +289,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { debug!("opaque handle found in handles manager: {:?}", path_str); let dir_path = Path::new(&path_str); debug!("path: {:?}", dir_path); + if dir_path.is_dir() { debug!("SftpServer ReadDir operation path = {:?}", dir_path); @@ -291,41 +298,17 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { StatusCode::SSH_FX_PERMISSION_DENIED })?; - debug!("got iterator = {:?}", dir_iterator); - - for entry in dir_iterator { - if let Ok(entry) = entry { - // info!("{:?}", entry); - if let Ok(metadata) = entry.metadata() { - if metadata.accessed().is_ok() - && metadata.modified().is_ok() - { - info!( - "{:?} : size = {:?}, uid = {:?}, gid = {:?}, \ - permissions = {:?}, atime = {:?}, mtime = {:?}, \ - ", - entry.path(), - metadata.len(), - metadata.st_uid(), - metadata.st_uid(), - metadata.permissions().mode(), - metadata - .accessed() - .unwrap() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(), - metadata - .modified() - .unwrap() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(), - ); - } - } - } - } + let name_entry_collection = DirEntriesCollection::new(dir_iterator); + + visitor.send_header( + name_entry_collection.get_count()?, + name_entry_collection.get_encoded_len()?, + ); + + name_entry_collection + .for_each_encoded(|data: &[u8]| visitor.send_item(data))?; + + // debug!("got iterator = {:?}", name_entry_collection); } else { error!("the path is not a directory = {:?}", dir_path); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); @@ -339,3 +322,115 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } } + +// TODO Add this to SFTP library only available with std as a global helper +#[derive(Debug)] +pub struct DirEntriesCollection { + /// Number of elements + count: u32, + /// Computed length of all the encoded elements + encoded_length: u32, + /// The actual entries. As you can see these are DirEntry. This is a std choice + entries: Vec, +} + +impl DirEntriesCollection { + pub fn new(dir_iterator: fs::ReadDir) -> Self { + let mut encoded_length = 0; + // This way I collect data required for the header and collect + // valid entries into a vector (only std) + let entries: Vec = dir_iterator + .filter_map(|entry_result| { + let entry = entry_result.ok()?; + + let filename = entry.path().to_string_lossy().into_owned(); + let name_entry = NameEntry { + filename: Filename::from(filename.as_str()), + _longname: Filename::from(""), + attrs: Self::get_attrs_or_empty(entry.metadata()), + }; + + const MAX_NAME_ENTRY_SIZE: usize = 4 + 256 + 4 + 72; // 4 + 256 bytes for path, 4 for empty long path and 72 bytes for the attributes ( 32/4*7 + 64/4 * 1 = 72) + + let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; + let mut sftp_sink = SftpSink::new(&mut buffer); + name_entry.enc(&mut sftp_sink).ok()?; + //TODO remove this unchecked casting + encoded_length += sftp_sink.payload_len() as u32; + Some(entry) + }) + .collect(); + + //TODO remove this unchecked casting + let count = entries.len() as u32; + + info!( + "Processed {} entries, estimated serialized length: {}", + count, encoded_length + ); + + Self { count, encoded_length, entries } + } + + fn get_attrs_or_empty( + maybe_metadata: Result, + ) -> Attrs { + maybe_metadata.map(Self::get_attrs).unwrap_or_default() + } + + fn get_attrs(metadata: fs::Metadata) -> Attrs { + let time_to_u32 = |time_result: io::Result| { + time_result + .ok()? + .duration_since(SystemTime::UNIX_EPOCH) + .ok()? + .as_secs() + .try_into() + .ok() + }; + + Attrs { + size: Some(metadata.len()), + uid: Some(metadata.st_uid()), + gid: Some(metadata.st_gid()), + permissions: Some(metadata.permissions().mode()), + atime: time_to_u32(metadata.accessed()), + mtime: time_to_u32(metadata.modified()), + ext_count: None, + } + } +} + +impl DirEntriesResponseHelpers for DirEntriesCollection { + fn get_count(&self) -> SftpOpResult { + Ok(self.count) + } + + fn get_encoded_len(&self) -> SftpOpResult { + Ok(self.encoded_length) + } + + fn for_each_encoded(&self, mut writer: F) -> SftpOpResult<()> + where + F: FnMut(&[u8]) -> (), + { + for entry in &self.entries { + let filename = entry.path().to_string_lossy().into_owned(); + let attrs = Self::get_attrs_or_empty(entry.metadata()); + let name_entry = NameEntry { + filename: Filename::from(filename.as_str()), + _longname: Filename::from(""), + attrs, + }; + debug!("Sending new item: {:?}", name_entry); + let mut buffer = [0u8; 4 + 256 + 4 + 72]; // 4 + 256 bytes for path, 4 for empty long path and 72 bytes for the attributes ( 32/4*7 + 64/4 * 1 = 72) + let mut sftp_sink = SftpSink::new(&mut buffer); + name_entry.enc(&mut sftp_sink).map_err(|err| { + debug!("WireError: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; + writer(sftp_sink.payload_slice()); + } + Ok(()) + } +} diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 740072c7..496bd475 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -69,10 +69,13 @@ pub use sftphandler::SftpHandler; /// Structures and types used to add the details for the target system pub mod server { + pub use crate::sftpserver::DirEntriesResponseHelpers; pub use crate::sftpserver::DirReply; pub use crate::sftpserver::ReadReply; pub use crate::sftpserver::SftpOpResult; pub use crate::sftpserver::SftpServer; + pub use crate::sftpsink::SftpSink; + pub use sunset::sshwire::SSHEncode; } /// Handles and helpers used by the [`sftpserver::SftpServer`] trait implementer diff --git a/sftp/src/sftphandler.rs b/sftp/src/sftphandler.rs index 94b8d213..b21bf936 100644 --- a/sftp/src/sftphandler.rs +++ b/sftp/src/sftphandler.rs @@ -5,6 +5,7 @@ use crate::proto::{ SftpPacket, Status, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; +use crate::server::DirReply; use crate::sftperror::SftpResult; use crate::sftpserver::SftpServer; use crate::sftpsink::SftpSink; @@ -487,6 +488,7 @@ where buffer_out: &mut [u8], ) -> SftpResult<()> { loop { + // let channelspub struct ChanOut<'g>(ChanIO<'g>) = stdio.split(); let lr = stdio.read(buffer_in).await?; trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); if lr == 0 { @@ -591,7 +593,14 @@ where SftpPacket::ReadDir(req_id, read_dir) => { // TODO Implement the mechanism you are going to use to // handle the list of elements - match file_server.readdir(&T::try_from(&read_dir.handle)?) { + + let mut muting = 0; + + let mut dir_reply = DirReply::mock(req_id, &mut muting); + + match file_server + .readdir(&T::try_from(&read_dir.handle)?, &mut dir_reply) + { Ok(_) => { todo!("Dance starts here"); } @@ -600,6 +609,7 @@ where push_unsupported(req_id, sink)?; } }; + debug!("final muting: {:?}", muting); error!("Unsupported Read Dir : {:?}", read_dir); // return Err(SftpError::NotSupported); // push_unsupported(ReqId(0), sink)?; diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 05cdfc22..70e7fe35 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,6 +1,8 @@ +use log::debug; + use crate::{ handles::OpaqueFileHandle, - proto::{Attrs, Name, StatusCode}, + proto::{Attrs, Name, ReqId, StatusCode}, }; use core::marker::PhantomData; @@ -72,7 +74,7 @@ where fn readdir( &mut self, opaque_dir_handle: &T, - // _reply: &mut DirReply<'_, '_>, + reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", @@ -88,6 +90,41 @@ where } } +/// This trait is an standardized way to interact with an iterator or collection of Directory entries +/// that need to be sent via an SSH_FXP_READDIR SFTP response to a client. +/// +/// It uses is expected when implementing an [`SftpServer`] TODO Future trait WIP +pub trait DirEntriesResponseHelpers { + /// returns the number of directory entries. + /// Used for the `SSH_FXP_READDIR` response field `count` + /// as specified in [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) + fn get_count(&self) -> SftpOpResult { + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + /// Returns the total encoded length in bytes for all directory entries. + /// Used for the `SSH_FXP_READDIR` general response field `length` + /// as part of the [General Packet Format](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-3) + /// + /// This represents the sum of all [`NameEntry`] structures when encoded + /// into [`SftpSink`] format. The length is to be pre-computed by + /// encoding each entry and summing the [`SftpSink::payload_len()`] values. + fn get_encoded_len(&self) -> SftpOpResult { + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter + /// were a [`NameEntry`] has been encoded. + /// + /// + fn for_each_encoded(&self, mut writer: F) -> SftpOpResult<()> + where + F: FnMut(&[u8]) -> (), + { + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } +} + // TODO Define this pub struct ReadReply<'g, 'a> { chan: ChanOut<'g, 'a>, @@ -99,15 +136,38 @@ impl<'g, 'a> ReadReply<'g, 'a> { // TODO Define this pub struct DirReply<'g, 'a> { + req_id: ReqId, + muting: &'a mut u32, chan: ChanOut<'g, 'a>, } impl<'g, 'a> DirReply<'g, 'a> { - pub fn reply(self, _data: &[u8]) {} + /// I am faking a DirReply to prototype it + pub fn mock(req_id: ReqId, muting: &'a mut u32) -> Self { + DirReply { + chan: ChanOut { _phantom_g: PhantomData, _phantom_a: PhantomData }, + muting, + req_id, + } + } + + /// mocks sending an item via a stdio + pub fn send_item(&mut self, data: &[u8]) { + *self.muting += 1; + debug!("Muted incremented {:?}. Got data: {:?}", self.muting, data); + } + + /// Must be call it first. Make this enforceable + pub fn send_header(&self, get_count: u32, get_encoded_len: u32) { + debug!( + "I will send the header here for request id {:?}: count = {:?}, length = {:?}", + self.req_id, get_count, get_encoded_len + ); + } } // TODO Implement correct Channel Out pub struct ChanOut<'g, 'a> { - _phantom_g: PhantomData<&'g ()>, - _phantom_a: PhantomData<&'a ()>, + _phantom_g: PhantomData<&'g ()>, // 'g look what these might be ChanIO lifetime + _phantom_a: PhantomData<&'a ()>, // a' Why the second lifetime if ChanIO only needs one } diff --git a/sftp/src/sftpsink.rs b/sftp/src/sftpsink.rs index ce55d265..377178c2 100644 --- a/sftp/src/sftpsink.rs +++ b/sftp/src/sftpsink.rs @@ -35,7 +35,7 @@ impl<'g> SftpSink<'g> { warn!("SftpSink trying to terminate it before pushing data"); return 0; } // size is 0 - let used_size = (self.index - SFTP_FIELD_LEN_LENGTH) as u32; + let used_size = self.payload_len() as u32; used_size .to_be_bytes() @@ -45,6 +45,16 @@ impl<'g> SftpSink<'g> { self.index } + + /// Auxiliary method to allow seen the len used by the encoded payload + pub fn payload_len(&self) -> usize { + self.index - SFTP_FIELD_LEN_LENGTH + } + + /// Auxiliary method to allow an immutable reference to the encoded payload + pub fn payload_slice(&self) -> &[u8] { + &self.buffer[SFTP_FIELD_LEN_LENGTH..self.payload_len()] + } } impl<'g> SSHSink for SftpSink<'g> { From 0f9fce03cffc36fd4f4b5a6d36e0bf71cb964dd8 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 16 Oct 2025 12:30:19 +1100 Subject: [PATCH 135/393] [skip ci] WIP: Refactored constant --- demo/sftp/std/src/demosftpserver.rs | 5 ++--- sftp/src/lib.rs | 4 ++++ sftp/src/proto.rs | 12 +++++++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index e4688d2b..f209da14 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -5,6 +5,7 @@ use crate::{ use sunset::sshwire::SSHEncode; use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; +use sunset_sftp::protocol::constants::MAX_NAME_ENTRY_SIZE; use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; use sunset_sftp::server::{ DirEntriesResponseHelpers, DirReply, ReadReply, SftpOpResult, SftpServer, @@ -350,8 +351,6 @@ impl DirEntriesCollection { attrs: Self::get_attrs_or_empty(entry.metadata()), }; - const MAX_NAME_ENTRY_SIZE: usize = 4 + 256 + 4 + 72; // 4 + 256 bytes for path, 4 for empty long path and 72 bytes for the attributes ( 32/4*7 + 64/4 * 1 = 72) - let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; let mut sftp_sink = SftpSink::new(&mut buffer); name_entry.enc(&mut sftp_sink).ok()?; @@ -423,7 +422,7 @@ impl DirEntriesResponseHelpers for DirEntriesCollection { attrs, }; debug!("Sending new item: {:?}", name_entry); - let mut buffer = [0u8; 4 + 256 + 4 + 72]; // 4 + 256 bytes for path, 4 for empty long path and 72 bytes for the attributes ( 32/4*7 + 64/4 * 1 = 72) + let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; let mut sftp_sink = SftpSink::new(&mut buffer); name_entry.enc(&mut sftp_sink).map_err(|err| { debug!("WireError: {:?}", err); diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 496bd475..0a9b81b0 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -94,6 +94,10 @@ pub mod protocol { pub use crate::proto::NameEntry; pub use crate::proto::PathInfo; pub use crate::proto::StatusCode; + /// Constants that might be useful for SFTP developers + pub mod constants { + pub use crate::proto::MAX_NAME_ENTRY_SIZE; + } } /// Errors and results used in this crate diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index de466721..24101601 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -27,15 +27,21 @@ pub const SFTP_FIELD_LEN_LENGTH: usize = 4; // SSH_FXP_WRITE SFTP Packet definition used to decode long packets that do not fit in one buffer -/// SFTP SSH_FXP_WRITE Packet cannot be shorter than this (len:4+pnum:1+rid:4+hand:4+0+data:4+0 bytes = 17 bytes) [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) +/// SFTP SSH_FXP_WRITE Packet cannot be shorter than this (len:4+pnum:1+rid:4+hand:4+0+data:4+0 bytes = 17 bytes) [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) // pub const SFTP_MINIMUM_WRITE_PACKET_LEN: usize = 17; -/// SFTP SSH_FXP_WRITE Packet request id field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) +/// SFTP SSH_FXP_WRITE Packet request id field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) pub const SFTP_WRITE_REQID_INDEX: usize = 5; -/// SFTP SSH_FXP_WRITE Packet handle field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) +/// SFTP SSH_FXP_WRITE Packet handle field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) // pub const SFTP_WRITE_HANDLE_INDEX: usize = 9; +/// Considering the definition in [Section 7](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) +/// for `SSH_FXP_READDIR` +/// +/// (4 + 256) bytes for path, (4 + 0) bytes for empty long path and 72 bytes for the attributes ( 32/4*7 + 64/4 * 1 = 72) +pub const MAX_NAME_ENTRY_SIZE: usize = 4 + 256 + 4 + 72; + // TODO is utf8 enough, or does this need to be an opaque binstring? /// See [SSH_FXP_NAME in Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) #[derive(Debug, SSHEncode, SSHDecode)] From 874e011f165b77d466faff6daa71b756c9ce7fd4 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 17 Oct 2025 14:18:02 +1100 Subject: [PATCH 136/393] [skip ci] WIP: Reworking a wrapper for Sink and ChanOut I want to use them grouped to later pass them to DirReply Also I might be able to use better the buffer out [skip ci] WIP: fixing some comments and warnings [skip ci] WIP: Ugly explorative refactoring [skip ci] WIP: Ugly explorative refactoring now working It is really ugly --- demo/sftp/std/src/demosftpserver.rs | 2 - demo/sftp/std/src/main.rs | 10 +- sftp/src/proto.rs | 3 +- sftp/src/sftphandler.rs | 326 ++++++++++++++++++---------- sftp/src/sftpserver.rs | 41 +++- sftp/src/sftpsink.rs | 29 ++- sftp/src/sftpsource.rs | 1 + 7 files changed, 289 insertions(+), 123 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index f209da14..5c6818ed 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -308,8 +308,6 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { name_entry_collection .for_each_encoded(|data: &[u8]| visitor.send_item(data))?; - - // debug!("got iterator = {:?}", name_entry_collection); } else { error!("the path is not a directory = {:?}", dir_path); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 74fccb3a..4de14d78 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -10,8 +10,6 @@ use crate::{ demoopaquefilehandle::DemoOpaqueFileHandle, demosftpserver::DemoSftpServer, }; -use embedded_io_async::{Read, Write}; - use embassy_executor::Spawner; use embassy_net::{Stack, StackResources, StaticConfigV4}; @@ -151,10 +149,10 @@ impl DemoServer for StdDemo { // TODO Do some research to find reasonable default buffer lengths let mut buffer_in = [0u8; 512]; let mut buffer_out = [0u8; 384]; - let mut incomplete_request_buffer = [0u8; 128]; // TODO Find a non arbitrary length + let mut incomplete_request_buffer = [0u8; 128]; match { - let mut stdio = serv.stdio(ch).await?; + let stdio = serv.stdio(ch).await?; let mut file_server = DemoSftpServer::new( "./demo/sftp/std/testing/out/".to_string(), ); @@ -164,8 +162,9 @@ impl DemoServer for StdDemo { &mut file_server, &mut incomplete_request_buffer, ); + sftp_handler - .process_loop(&mut stdio, &mut buffer_in, &mut buffer_out) + .process_loop(stdio, &mut buffer_in, &mut buffer_out) .await?; Ok::<_, Error>(()) @@ -212,6 +211,7 @@ async fn main(spawner: Spawner) { env_logger::builder() .filter_level(log::LevelFilter::Debug) .filter_module("sunset::runner", log::LevelFilter::Info) + // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Trace) .filter_module("sunset::traffic", log::LevelFilter::Info) .filter_module("sunset::encrypt", log::LevelFilter::Info) .filter_module("sunset::conn", log::LevelFilter::Info) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 24101601..a1bddeed 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -688,7 +688,7 @@ macro_rules! sftpmessages { } - /// Decode a request. Includes Initialization packets + /// Decode a request or initialization packets /// /// Used by a SFTP server. Does not include the length field. /// @@ -792,6 +792,5 @@ sftpmessages! [ (102, Handle, Handle<'a>, "ssh_fxp_handle"), (103, Data, Data<'a>, "ssh_fxp_data"), (104, Name, Name<'a>, "ssh_fxp_name"), - }, ]; diff --git a/sftp/src/sftphandler.rs b/sftp/src/sftphandler.rs index b21bf936..014d3140 100644 --- a/sftp/src/sftphandler.rs +++ b/sftp/src/sftphandler.rs @@ -12,8 +12,8 @@ use crate::sftpsink::SftpSink; use crate::sftpsource::SftpSource; use sunset::Error as SunsetError; -use sunset::sshwire::{SSHSource, WireError, WireResult}; -use sunset_async::ChanInOut; +use sunset::sshwire::{SSHEncode, SSHSource, WireError, WireResult}; +use sunset_async::{ChanInOut, ChanOut}; use core::u32; use embedded_io_async::{Read, Write}; @@ -39,7 +39,11 @@ enum FragmentedRequestState { /// and more bytes are needed ProcessingClippedRequest, /// A request, with a length over the incoming buffer capacity is being - /// processed + /// processed. + /// + /// E.g. a write request with size exceeding the + /// buffer size: Processing this request will require to be split + /// into multiple write actions ProcessingLongRequest, } @@ -126,6 +130,8 @@ where } } + /// WIP: A version of process that takes a chan_out to write the data + /// /// Maintaining an internal status: /// /// - Decodes the buffer_in request @@ -135,15 +141,13 @@ where /// /// **Returns**: A result containing the number of bytes used in /// `buffer_out` - pub async fn process( + async fn process<'g>( &mut self, buffer_in: &[u8], - buffer_out: &mut [u8], - ) -> SftpResult { + output_wrapper: &mut OutputWrapper<'a, 'g>, + ) -> SftpResult<()> { let in_len = buffer_in.len(); - let mut buffer_in_remaining_index = 0; - - let mut used_out_accumulated_index = 0; + let mut buffer_in_lower_index_bracket = 0; trace!("Received {:} bytes to process", in_len); @@ -153,23 +157,25 @@ where return Err(WireError::PacketWrong.into()); } - while buffer_in_remaining_index < in_len { - let mut sink = - SftpSink::new(&mut buffer_out[used_out_accumulated_index..]); - + while buffer_in_lower_index_bracket < in_len { + debug!( + "Buffer In Lower index bracket: {}", + buffer_in_lower_index_bracket + ); debug!( "<=======================[ SFTP Process State: {:?} ]=======================>", self.state ); match &self.state { + // There is a fragmented request in process of processing SftpHandleState::Fragmented(fragment_case) => { match fragment_case { FragmentedRequestState::ProcessingClippedRequest => { if let Err(e) = self .incomplete_request_holder .try_append_for_valid_request( - &buffer_in[buffer_in_remaining_index..], + &buffer_in[buffer_in_lower_index_bracket..], ) { match e { @@ -178,7 +184,7 @@ where "There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); - buffer_in_remaining_index += self + buffer_in_lower_index_bracket += self .incomplete_request_holder .appended(); continue; @@ -190,7 +196,7 @@ where "WIRE ERROR: There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); - buffer_in_remaining_index += self + buffer_in_lower_index_bracket += self .incomplete_request_holder .appended(); continue; @@ -216,7 +222,7 @@ where } let used = self.incomplete_request_holder.appended(); - buffer_in_remaining_index += used; + buffer_in_lower_index_bracket += used; let mut source = SftpSource::new( &self.incomplete_request_holder.try_get_ref()?, @@ -227,39 +233,37 @@ where Ok(request) => { Self::handle_general_request( &mut self.file_server, - &mut sink, + output_wrapper, request, - )?; + ) + .await?; self.incomplete_request_holder.reset(); self.state = SftpHandleState::Idle; } Err(e) => match e { - WireError::RanOut => { - match Self::handle_ran_out( - &mut self.file_server, - &mut sink, - &mut source, - ) { - Ok(holder) => { - self.partial_write_request_tracker = - Some(holder); - self.incomplete_request_holder - .reset(); - self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); - } - Err(e) => match e { - _ => { - error!( - "handle_ran_out finished with error: {:?}", - e - ); - return Err( - SunsetError::Bug.into() - ); - } - }, + WireError::RanOut => match Self::handle_ran_out( + &mut self.file_server, + output_wrapper, + &mut source, + ) + .await + { + Ok(holder) => { + self.partial_write_request_tracker = + Some(holder); + self.incomplete_request_holder.reset(); + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); } - } + Err(e) => match e { + _ => { + error!( + "handle_ran_out finished with error: {:?}", + e + ); + return Err(SunsetError::Bug.into()); + } + }, + }, WireError::NoRoom => { error!("Not enough space to fit the request") } @@ -275,7 +279,7 @@ where } FragmentedRequestState::ProcessingLongRequest => { let mut source = SftpSource::new( - &buffer_in[buffer_in_remaining_index..], + &buffer_in[buffer_in_lower_index_bracket..], ); trace!("Source content: {:?}", source); @@ -333,7 +337,11 @@ where self.partial_write_request_tracker = Some(write_tracker); } else { - push_ok(write_tracker.req_id, &mut sink)?; + push_ok( + write_tracker.req_id, + &mut output_wrapper.get_mut_sink_ref(), + )?; + output_wrapper.send_buffer().await?; info!("Finished multi part Write Request"); self.state = SftpHandleState::Idle; } @@ -343,29 +351,26 @@ where push_general_failure( write_tracker.req_id, "error writing", - &mut sink, + &mut output_wrapper.get_mut_sink_ref(), )?; + output_wrapper.send_buffer().await?; self.state = SftpHandleState::Idle; } }; - buffer_in_remaining_index = in_len - source.remaining(); + buffer_in_lower_index_bracket = + in_len - source.remaining(); } } } + // No pending request _ => { let mut source = - SftpSource::new(&buffer_in[buffer_in_remaining_index..]); - trace!("Source content: {:?}", source); + SftpSource::new(&buffer_in[buffer_in_lower_index_bracket..]); let sftp_packet = SftpPacket::decode_request(&mut source); match self.state { - SftpHandleState::Fragmented(_) => { - return Err( - SftpError::SunsetError(SunsetError::Bug).into() - ); - } SftpHandleState::Initializing => match sftp_packet { Ok(request) => { match request { @@ -375,8 +380,11 @@ where version: SFTP_VERSION, }); - info!("Sending '{:?}'", version); - version.encode_response(&mut sink)?; + // info!("Sending '{:?}'", version); + version.encode_response( + output_wrapper.get_mut_sink_ref(), + )?; + output_wrapper.send_buffer().await?; self.state = SftpHandleState::Idle; } _ => { @@ -399,12 +407,12 @@ where SftpHandleState::Idle => { match sftp_packet { Ok(request) => { - // self.handle_general_request(&mut sink, request)? Self::handle_general_request( &mut self.file_server, - &mut sink, + output_wrapper, request, - )?; + ) + .await?; } Err(e) => match e { WireError::RanOut => { @@ -415,9 +423,11 @@ where match Self::handle_ran_out( &mut self.file_server, - &mut sink, + output_wrapper, &mut source, - ) { + ) + .await + { Ok(holder) => { self.partial_write_request_tracker = Some(holder); @@ -433,9 +443,9 @@ where let read = self.incomplete_request_holder .try_hold( &buffer_in - [buffer_in_remaining_index..], + [buffer_in_lower_index_bracket..], )?; - buffer_in_remaining_index += + buffer_in_lower_index_bracket += read; self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); continue; @@ -449,13 +459,6 @@ where } }; } - WireError::UnknownPacket { number: _ } => { - warn!("Error decoding SFTP Packet:{:?}", e); - push_unsupported( - ReqId(u32::MAX), - &mut sink, - )?; - } _ => { error!( "Error decoding SFTP Packet: {:?}", @@ -463,63 +466,62 @@ where ); push_unsupported( ReqId(u32::MAX), - &mut sink, + &mut output_wrapper.get_mut_sink_ref(), )?; + output_wrapper.send_buffer().await?; } }, }; } + _ => { + error!( + "Unhandled SftpHandleState {:?} in main loop", + self.state + ); + return Err(SunsetError::Bug.into()); + } } - buffer_in_remaining_index = in_len - source.remaining(); + buffer_in_lower_index_bracket = in_len - source.remaining(); } }; - used_out_accumulated_index += sink.finalize(); } - Ok(used_out_accumulated_index) + Ok(()) } /// WIP: A loop that will process all the request from stdio until /// an EOF is received pub async fn process_loop<'c>( &mut self, - stdio: &mut ChanInOut<'c>, + stdio: ChanInOut<'c>, buffer_in: &mut [u8], - buffer_out: &mut [u8], + buffer_out: &'a mut [u8], ) -> SftpResult<()> { + let (mut chan_in, chan_out) = stdio.split(); + + let mut out_wrapper = OutputWrapper::new(buffer_out, chan_out); loop { - // let channelspub struct ChanOut<'g>(ChanIO<'g>) = stdio.split(); - let lr = stdio.read(buffer_in).await?; + let lr = chan_in.read(buffer_in).await?; trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); if lr == 0 { debug!("client disconnected"); return Err(SftpError::ClientDisconnected); } - let lw = self.process(&buffer_in[0..lr], buffer_out).await?; - if lw > 0 { - let wo = stdio.write(&mut buffer_out[0..lw]).await?; - if wo != lw { - error!("SFTP ----> Sent incomplete {} < {}", wo, lw); - todo!( - "Fix write incomplete condition: Repeat until \ - all is gone down the channel" - ); - } - trace!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); - } + self.process(&buffer_in[0..lr], &mut out_wrapper).await?; } - Ok(()) + // Ok(()) } - fn handle_general_request( + async fn handle_general_request<'g>( file_server: &mut S, - sink: &mut SftpSink<'_>, + output_wrapper: &mut OutputWrapper<'a, 'g>, request: SftpPacket<'_>, ) -> Result<(), SftpError> where T: OpaqueFileHandle, { + debug!("Handling general request: {:?}", request); match request { SftpPacket::Init(_) => { error!("The Init packet is not a request but an initialization"); @@ -529,8 +531,14 @@ where let a_name = file_server.realpath(path_info.path.as_str()?)?; let response = SftpPacket::Name(req_id, a_name); + debug!( + "Request Id {:?}. Encoding response: {:?}", + &req_id, &response + ); - response.encode_response(sink)?; + response.encode_response(output_wrapper.get_mut_sink_ref())?; + // dbg!("PathInfo response encoded", &response); + output_wrapper.send_buffer().await?; } SftpPacket::Open(req_id, open) => { match file_server.open(open.filename.as_str()?, &open.attrs) { @@ -541,34 +549,58 @@ where handle: opaque_file_handle.into_file_handle(), }, ); - response.encode_response(sink)?; - info!("Sending '{:?}'", response); + response + .encode_response(output_wrapper.get_mut_sink_ref())?; + output_wrapper.send_buffer().await?; } Err(status_code) => { error!("Open failed: {:?}", status_code); - push_general_failure(req_id, "", sink)?; + push_general_failure( + req_id, + "", + output_wrapper.get_mut_sink_ref(), + )?; + output_wrapper.send_buffer().await?; } }; } + // TODO The visitor behavioral pattern could be use in write to speed-up + // the writing process SftpPacket::Write(req_id, write) => { match file_server.write( &T::try_from(&write.handle)?, write.offset, write.data.as_ref(), ) { - Ok(_) => push_ok(req_id, sink)?, + Ok(_) => { + push_ok(req_id, output_wrapper.get_mut_sink_ref())?; + output_wrapper.send_buffer().await?; + } Err(e) => { error!("SFTP write thrown: {:?}", e); - push_general_failure(req_id, "error writing", sink)? + push_general_failure( + req_id, + "error writing", + output_wrapper.get_mut_sink_ref(), + )?; + output_wrapper.send_buffer().await?; } }; } SftpPacket::Close(req_id, close) => { match file_server.close(&T::try_from(&close.handle)?) { - Ok(_) => push_ok(req_id, sink)?, + Ok(_) => { + push_ok(req_id, output_wrapper.get_mut_sink_ref())?; + output_wrapper.send_buffer().await?; + } Err(e) => { error!("SFTP Close thrown: {:?}", e); - push_general_failure(req_id, "", sink)? + push_general_failure( + req_id, + "", + output_wrapper.get_mut_sink_ref(), + )?; + output_wrapper.send_buffer().await?; } } } @@ -581,12 +613,18 @@ where handle: opaque_file_handle.into_file_handle(), }, ); - response.encode_response(sink)?; - info!("Sending '{:?}'", response); + response + .encode_response(output_wrapper.get_mut_sink_ref())?; + output_wrapper.send_buffer().await?; } Err(status_code) => { error!("Open failed: {:?}", status_code); - push_general_failure(req_id, "", sink)?; + push_general_failure( + req_id, + "", + output_wrapper.get_mut_sink_ref(), + )?; + output_wrapper.send_buffer().await?; } }; } @@ -606,7 +644,8 @@ where } Err(status_code) => { error!("Open failed: {:?}", status_code); - push_unsupported(req_id, sink)?; + push_unsupported(req_id, output_wrapper.get_mut_sink_ref())?; + output_wrapper.send_buffer().await?; } }; debug!("final muting: {:?}", muting); @@ -615,7 +654,7 @@ where // push_unsupported(ReqId(0), sink)?; } _ => { - error!("Unsuported request type: {:?}", request); + error!("Unsupported request type: {:?}", request); return Err(SftpError::NotSupported); // push_unsupported(ReqId(0), sink)?; } @@ -628,9 +667,9 @@ where /// /// **WARNING:** Only `SSH_FXP_WRITE` has been implemented! /// - fn handle_ran_out( + async fn handle_ran_out<'g>( file_server: &mut S, - sink: &mut SftpSink<'_>, + output_wrapper: &mut OutputWrapper<'a, 'g>, source: &mut SftpSource<'_>, ) -> SftpResult> { let packet_type = source.peak_packet_type()?; @@ -673,7 +712,12 @@ where } Err(e) => { error!("SFTP write thrown: {:?}", e); - push_general_failure(req_id, "error writing ", sink)?; + push_general_failure( + req_id, + "error writing ", + output_wrapper.get_mut_sink_ref(), + )?; + output_wrapper.send_buffer().await?; return Err(SftpError::FileServerError(e)); } }; @@ -690,6 +734,68 @@ where } } +struct OutputWrapper<'a, 'g> { + sink: SftpSink<'a>, + channel_out: ChanOut<'g>, +} + +impl<'a, 'g> OutputWrapper<'a, 'g> { + pub fn new(buffer: &'a mut [u8], channel_out: ChanOut<'g>) -> Self { + let sink = SftpSink::new(buffer); + OutputWrapper { channel_out, sink } + } + + pub fn reset(&mut self) { + self.sink.reset(); + } + + pub fn get_mut_sink_ref(&mut self) -> &mut SftpSink<'a> { + &mut self.sink + } + + // TODO Are we using this? + pub fn encode(&mut self, data: &T) -> SftpResult<()> + where + T: SSHEncode, + { + data.enc(&mut self.sink)?; + Ok(()) + } + // + + /// Finalizes (Prepends the packet length) and send the data in the + /// buffer by the subsystem channel out + pub async fn send_buffer(&mut self) -> SftpResult { + if self.sink.payload_len() == 0 { + debug!("No data to send in the SFTP sink"); + return Ok(0); + } + self.sink.finalize(); + let buffer = self.sink.used_slice(); + info!("Sending buffer: '{:?}'", buffer); + let written = self.channel_out.write(buffer).await?; + self.sink.reset(); + Ok(written) + } + + /// Send the data in the buffer by the subsystem channel out without + /// prepending the packet length to it. + /// + /// This is useful when an SFTP packet header has already being sent + /// or when the data requires an special treatment + pub async fn send_payload(&mut self) -> SftpResult { + let payload = self.sink.payload_slice(); + info!("Sending payload: '{:?}'", payload); + let written = self.channel_out.write(payload).await?; + self.sink.reset(); + Ok(written) + } + + pub fn finalize(&mut self) -> usize { + self.sink.finalize() + } +} + #[inline] fn push_ok(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireError> { let response = SftpPacket::Status( diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 70e7fe35..895605c4 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -6,6 +6,7 @@ use crate::{ }; use core::marker::PhantomData; +// use futures::executor::block_on; TODO Deal with the async nature of [`ChanOut`] /// Result used to store the result of an Sftp Operation pub type SftpOpResult = core::result::Result; @@ -71,6 +72,7 @@ where } /// Reads the list of items in a directory + #[allow(unused_variables)] fn readdir( &mut self, opaque_dir_handle: &T, @@ -117,7 +119,8 @@ pub trait DirEntriesResponseHelpers { /// were a [`NameEntry`] has been encoded. /// /// - fn for_each_encoded(&self, mut writer: F) -> SftpOpResult<()> + #[allow(unused_variables)] + fn for_each_encoded(&self, writer: F) -> SftpOpResult<()> where F: FnMut(&[u8]) -> (), { @@ -126,17 +129,49 @@ pub trait DirEntriesResponseHelpers { } // TODO Define this +/// **This is a work in progress** +/// A reference structure passed to the [`SftpServer::read()`] method to +/// allow replying with the read data. + pub struct ReadReply<'g, 'a> { chan: ChanOut<'g, 'a>, } impl<'g, 'a> ReadReply<'g, 'a> { - pub fn reply(self, _data: &[u8]) {} + /// **This is a work in progress** + /// + /// Reply with a slice containing the read data + /// It can be called several times to send multiple data chunks + /// + /// **Important**: The first reply should contain the header + #[allow(unused_variables)] + pub fn reply(self, data: &[u8]) {} } // TODO Define this +/// Dir Reply is the structure that will be "visiting" the [`SftpServer`] +/// trait +/// implementation via [`SftpServer::readdir()`] in order to send the +/// directory content list. +/// +/// It contains references to a slice buffer and a output channel +/// [`sunset_async::async_channel::ChanOut`] used in the context of an +/// SFTP Session. +/// +/// The usage is simple: +/// +/// 1. SftpHandler will: Initialize the structure with the buffer and the ChanOut +/// 2. The `SftpServer` trait implementation for `readdir()` will: +/// a. Receive the DirReply +/// b. Obtain the number of items and the **total** encoded length of +/// the [`NameEntry`] items in the directory and send them calling `send_header` +/// c. Send each serialized `NameEntry`, excluding their length using +/// the `send_item` method +/// pub struct DirReply<'g, 'a> { + /// Used during the req_id: ReqId, + /// To test muting operations muting: &'a mut u32, chan: ChanOut<'g, 'a>, } @@ -151,12 +186,14 @@ impl<'g, 'a> DirReply<'g, 'a> { } } + // TODO this will need to do async execution /// mocks sending an item via a stdio pub fn send_item(&mut self, data: &[u8]) { *self.muting += 1; debug!("Muted incremented {:?}. Got data: {:?}", self.muting, data); } + // TODO this will need to do async execution /// Must be call it first. Make this enforceable pub fn send_header(&self, get_count: u32, get_encoded_len: u32) { debug!( diff --git a/sftp/src/sftpsink.rs b/sftp/src/sftpsink.rs index 377178c2..08fa7f9e 100644 --- a/sftp/src/sftpsink.rs +++ b/sftp/src/sftpsink.rs @@ -12,7 +12,7 @@ use log::{debug, error, info, log, trace, warn}; /// len #[derive(Default)] pub struct SftpSink<'g> { - pub buffer: &'g mut [u8], + buffer: &'g mut [u8], index: usize, } @@ -52,8 +52,32 @@ impl<'g> SftpSink<'g> { } /// Auxiliary method to allow an immutable reference to the encoded payload + /// excluding the `u32` length field prepended to it pub fn payload_slice(&self) -> &[u8] { - &self.buffer[SFTP_FIELD_LEN_LENGTH..self.payload_len()] + &self.buffer + [SFTP_FIELD_LEN_LENGTH..SFTP_FIELD_LEN_LENGTH + self.payload_len()] + } + + /// Auxiliary method to allow an immutable reference to the full used + /// data (includes the prepended length field) + /// + /// **Important:** Call this after [`SftpSink::finalize()`] + pub fn used_slice(&self) -> &[u8] { + debug!( + "SftpSink used_slice called, total len: {}. Index: {}", + SFTP_FIELD_LEN_LENGTH + self.payload_len(), + self.index + ); + &self.buffer[..SFTP_FIELD_LEN_LENGTH + self.payload_len()] + } + + /// Reset the index and cleans the length field + pub fn reset(&mut self) -> () { + debug!("SftpSink reset called when index was {:?}", self.index); + self.index = SFTP_FIELD_LEN_LENGTH; + for i in 0..SFTP_FIELD_LEN_LENGTH { + self.buffer[i] = 0; + } } } @@ -64,6 +88,7 @@ impl<'g> SSHSink for SftpSink<'g> { } trace!("Sink index: {:}", self.index); v.iter().for_each(|val| { + trace!("Writing val {:} at index {:}", *val, self.index); self.buffer[self.index] = *val; self.index += 1; }); diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 67b743e8..7e1918e3 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -47,6 +47,7 @@ impl<'de> SSHSource<'de> for SftpSource<'de> { impl<'de> SftpSource<'de> { /// Creates a new [`SftpSource`] referencing a buffer pub fn new(buffer: &'de [u8]) -> Self { + debug!("New source with content: : {:?}", buffer); SftpSource { buffer: buffer, index: 0 } } From 4f9b8a874c8789263fbde887292457e44453165e Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 22 Oct 2025 18:36:02 +1100 Subject: [PATCH 137/393] [skip ci] WIP: Refactoring. Creating a mod for sftphandler [skip ci] WIP: Refactoring. Normalizing push function Unifying around push_status [skip ci] WIP: Refactoring. Integrating push functions in OutputWrapper - push_status - push_packet [skip ci] WIP: Refactoring. Moving SftpOutputChannelWrapper (aka OutputWrapper) to its own module cleaning up sftphandle.rs [skip ci] WIP: Refactoring - SftpHandler: estate machine restructuring - SftpServer: WIP for DirReply - SftpOutputChannelWrapper: WIP refactoring --- demo/sftp/std/src/main.rs | 3 +- sftp/src/sftphandler/mod.rs | 7 + .../sftphandler/partialwriterequesttracker.rs | 60 ++ sftp/src/{ => sftphandler}/sftphandler.rs | 569 +++++++----------- .../sftphandler/sftpoutputchannelwrapper.rs | 94 +++ sftp/src/sftpserver.rs | 54 +- 6 files changed, 430 insertions(+), 357 deletions(-) create mode 100644 sftp/src/sftphandler/mod.rs create mode 100644 sftp/src/sftphandler/partialwriterequesttracker.rs rename sftp/src/{ => sftphandler}/sftphandler.rs (60%) create mode 100644 sftp/src/sftphandler/sftpoutputchannelwrapper.rs diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 4de14d78..4052cbb8 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -211,7 +211,8 @@ async fn main(spawner: Spawner) { env_logger::builder() .filter_level(log::LevelFilter::Debug) .filter_module("sunset::runner", log::LevelFilter::Info) - // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Trace) + .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) + .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) .filter_module("sunset::traffic", log::LevelFilter::Info) .filter_module("sunset::encrypt", log::LevelFilter::Info) .filter_module("sunset::conn", log::LevelFilter::Info) diff --git a/sftp/src/sftphandler/mod.rs b/sftp/src/sftphandler/mod.rs new file mode 100644 index 00000000..fc32ff97 --- /dev/null +++ b/sftp/src/sftphandler/mod.rs @@ -0,0 +1,7 @@ +mod partialwriterequesttracker; +mod sftphandler; +mod sftpoutputchannelwrapper; + +pub use partialwriterequesttracker::PartialWriteRequestTracker; +pub use sftphandler::SftpHandler; +pub use sftpoutputchannelwrapper::SftpOutputChannelWrapper; diff --git a/sftp/src/sftphandler/partialwriterequesttracker.rs b/sftp/src/sftphandler/partialwriterequesttracker.rs new file mode 100644 index 00000000..72acbfe4 --- /dev/null +++ b/sftp/src/sftphandler/partialwriterequesttracker.rs @@ -0,0 +1,60 @@ +use crate::handles::OpaqueFileHandle; +use crate::proto::ReqId; +use sunset::sshwire::WireResult; + +// TODO Generalize this to allow other request types +/// Used to keep record of a long SFTP Write request that does not fit in +/// receiving buffer and requires processing in batches +#[derive(Debug)] +pub struct PartialWriteRequestTracker { + req_id: ReqId, + opaque_handle: T, + remain_data_len: u32, + remain_data_offset: u64, +} + +impl PartialWriteRequestTracker { + /// Creates a new [`PartialWriteRequestTracker`] + pub fn new( + req_id: ReqId, + opaque_handle: T, + remain_data_len: u32, + remain_data_offset: u64, + ) -> WireResult { + Ok(PartialWriteRequestTracker { + req_id, + opaque_handle: opaque_handle, + remain_data_len, + remain_data_offset, + }) + } + /// Returns the opaque file handle associated with the request + /// tracked + pub fn get_opaque_file_handle(&self) -> T { + self.opaque_handle.clone() + } + + pub fn get_remain_data_len(&self) -> u32 { + self.remain_data_len + } + + pub fn get_remain_data_offset(&self) -> u64 { + self.remain_data_offset + } + + // pub fn add_to_remain_data_offset(&mut self, add_offset: u64) { + // self.remain_data_offset += add_offset; + // } + + pub(crate) fn update_remaining_after_partial_write( + &mut self, + data_segment_len: u32, + ) -> () { + self.remain_data_offset += data_segment_len as u64; + self.remain_data_len -= data_segment_len; + } + + pub(crate) fn get_req_id(&self) -> ReqId { + self.req_id.clone() // TODO reference? + } +} diff --git a/sftp/src/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs similarity index 60% rename from sftp/src/sftphandler.rs rename to sftp/src/sftphandler/sftphandler.rs index 014d3140..71969f4b 100644 --- a/sftp/src/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -1,22 +1,24 @@ +use super::PartialWriteRequestTracker; + use crate::error::SftpError; use crate::handles::OpaqueFileHandle; use crate::proto::{ self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, - SftpPacket, Status, StatusCode, + SftpPacket, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; use crate::server::DirReply; use crate::sftperror::SftpResult; +use crate::sftphandler::sftpoutputchannelwrapper::SftpOutputChannelWrapper; use crate::sftpserver::SftpServer; -use crate::sftpsink::SftpSink; use crate::sftpsource::SftpSource; use sunset::Error as SunsetError; -use sunset::sshwire::{SSHEncode, SSHSource, WireError, WireResult}; -use sunset_async::{ChanInOut, ChanOut}; +use sunset::sshwire::{SSHSource, WireError}; +use sunset_async::ChanInOut; use core::u32; -use embedded_io_async::{Read, Write}; +use embedded_io_async::Read; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; @@ -47,38 +49,38 @@ enum FragmentedRequestState { ProcessingLongRequest, } -// TODO Generalize this to allow other request types -/// Used to keep record of a long SFTP Write request that does not fit in -/// receiving buffer and requires processing in batches -#[derive(Debug)] -pub struct PartialWriteRequestTracker { - req_id: ReqId, - opaque_handle: T, - remain_data_len: u32, - remain_data_offset: u64, -} - -impl PartialWriteRequestTracker { - /// Creates a new [`PartialWriteRequestTracker`] - pub fn new( - req_id: ReqId, - opaque_handle: T, - remain_data_len: u32, - remain_data_offset: u64, - ) -> WireResult { - Ok(PartialWriteRequestTracker { - req_id, - opaque_handle: opaque_handle, - remain_data_len, - remain_data_offset, - }) - } - /// Returns the opaque file handle associated with the request - /// tracked - pub fn get_opaque_file_handle(&self) -> T { - self.opaque_handle.clone() - } -} +// // TODO Generalize this to allow other request types +// /// Used to keep record of a long SFTP Write request that does not fit in +// /// receiving buffer and requires processing in batches +// #[derive(Debug)] +// pub struct PartialWriteRequestTracker { +// req_id: ReqId, +// opaque_handle: T, +// remain_data_len: u32, +// remain_data_offset: u64, +// } + +// impl PartialWriteRequestTracker { +// /// Creates a new [`PartialWriteRequestTracker`] +// pub fn new( +// req_id: ReqId, +// opaque_handle: T, +// remain_data_len: u32, +// remain_data_offset: u64, +// ) -> WireResult { +// Ok(PartialWriteRequestTracker { +// req_id, +// opaque_handle: opaque_handle, +// remain_data_len, +// remain_data_offset, +// }) +// } +// /// Returns the opaque file handle associated with the request +// /// tracked +// pub fn get_opaque_file_handle(&self) -> T { +// self.opaque_handle.clone() +// } +// } /// Process the raw buffers in and out from a subsystem channel decoding /// request and encoding responses @@ -144,7 +146,7 @@ where async fn process<'g>( &mut self, buffer_in: &[u8], - output_wrapper: &mut OutputWrapper<'a, 'g>, + output_wrapper: &mut SftpOutputChannelWrapper<'a, 'g>, ) -> SftpResult<()> { let in_len = buffer_in.len(); let mut buffer_in_lower_index_bracket = 0; @@ -202,9 +204,12 @@ where continue; } RequestHolderError::NoRoom => { - warn!( - "The request holder if full but the request in incomplete" - ) + error!( + "The request holder if full but the request in incomplete. \ + Consider increasing its size" + ); + // TODO react to this situation with an internal server error + return Err(SunsetError::NoRoom {}.into()); } _ => { @@ -299,7 +304,7 @@ where let usable_data = source .remaining() - .min(write_tracker.remain_data_len as usize); + .min(write_tracker.get_remain_data_len() as usize); let data_segment = // Fails!! source.dec_as_binstring(usable_data)?; @@ -312,10 +317,10 @@ where SunsetError::Bug })?; let current_write_offset = - write_tracker.remain_data_offset; - write_tracker.remain_data_offset += - data_segment_len as u64; - write_tracker.remain_data_len -= data_segment_len; + write_tracker.get_remain_data_offset(); + write_tracker.update_remaining_after_partial_write( + data_segment_len, + ); debug!( "Processing successive chunks of a long write packet. \ @@ -324,7 +329,7 @@ where opaque_handle, current_write_offset, data_segment, - write_tracker.remain_data_len + write_tracker.get_remain_data_len() ); match self.file_server.write( @@ -333,27 +338,30 @@ where data_segment.as_ref(), ) { Ok(_) => { - if write_tracker.remain_data_len > 0 { + if write_tracker.get_remain_data_len() > 0 { self.partial_write_request_tracker = Some(write_tracker); } else { - push_ok( - write_tracker.req_id, - &mut output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status( + write_tracker.get_req_id(), + StatusCode::SSH_FX_OK, + "", + ) + .await?; info!("Finished multi part Write Request"); self.state = SftpHandleState::Idle; } } Err(e) => { error!("SFTP write thrown: {:?}", e); - push_general_failure( - write_tracker.req_id, - "error writing", - &mut output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status( + write_tracker.get_req_id(), + StatusCode::SSH_FX_FAILURE, + "error writing", + ) + .await?; self.state = SftpHandleState::Idle; } }; @@ -363,127 +371,114 @@ where } } - // No pending request - _ => { - let mut source = - SftpSource::new(&buffer_in[buffer_in_lower_index_bracket..]); - - let sftp_packet = SftpPacket::decode_request(&mut source); - - match self.state { - SftpHandleState::Initializing => match sftp_packet { - Ok(request) => { - match request { - SftpPacket::Init(_) => { - let version = - SftpPacket::Version(InitVersionLowest { - version: SFTP_VERSION, - }); - - // info!("Sending '{:?}'", version); - version.encode_response( - output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; - self.state = SftpHandleState::Idle; - } - _ => { - error!( - "Request received before init: {:?}", - request - ); - return Err(SftpError::NotInitialized); - } - }; - } - Err(_) => { - error!( - "Malformed SFTP Packet before Init: {:?}", - sftp_packet - ); - return Err(SftpError::MalformedPacket); - } - }, - SftpHandleState::Idle => { - match sftp_packet { - Ok(request) => { - Self::handle_general_request( - &mut self.file_server, - output_wrapper, - request, - ) - .await?; + SftpHandleState::Initializing => { + let (source, sftp_packet) = create_sftp_source_and_packet( + buffer_in, + buffer_in_lower_index_bracket, + ); + match sftp_packet { + Ok(request) => { + match request { + SftpPacket::Init(_) => { + let version = + SftpPacket::Version(InitVersionLowest { + version: SFTP_VERSION, + }); + + output_wrapper.send_packet(version).await?; + self.state = SftpHandleState::Idle; } - Err(e) => match e { - WireError::RanOut => { - warn!( - "RanOut for the SFTP Packet in the source buffer: {:?}", - e - ); + _ => { + error!( + "Request received before init: {:?}", + request + ); + return Err(SftpError::NotInitialized); + } + }; + } + Err(_) => { + error!( + "Malformed SFTP Packet before Init: {:?}", + sftp_packet + ); + return Err(SftpError::MalformedPacket); + } + } + buffer_in_lower_index_bracket = in_len - source.remaining(); + } + SftpHandleState::Idle => { + let (mut source, sftp_packet) = create_sftp_source_and_packet( + buffer_in, + buffer_in_lower_index_bracket, + ); + match sftp_packet { + Ok(request) => { + Self::handle_general_request( + &mut self.file_server, + output_wrapper, + request, + ) + .await?; + } + Err(e) => match e { + WireError::RanOut => { + warn!( + "RanOut for the SFTP Packet in the source buffer: {:?}", + e + ); - match Self::handle_ran_out( - &mut self.file_server, - output_wrapper, - &mut source, - ) - .await - { - Ok(holder) => { - self.partial_write_request_tracker = - Some(holder); - self.state = + match Self::handle_ran_out( + &mut self.file_server, + output_wrapper, + &mut source, + ) + .await + { + Ok(holder) => { + self.partial_write_request_tracker = + Some(holder); + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) - } - Err(e) => { - error!("Error handle_ran_out"); - match e { - SftpError::WireError( - WireError::RanOut, - ) => { - let read = self.incomplete_request_holder + } + Err(e) => { + error!("Error handle_ran_out"); + match e { + SftpError::WireError( + WireError::RanOut, + ) => { + let read = self.incomplete_request_holder .try_hold( &buffer_in [buffer_in_lower_index_bracket..], )?; - buffer_in_lower_index_bracket += - read; - self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); - continue; - } - _ => { - return Err( - SunsetError::Bug.into(), - ); - } - } + buffer_in_lower_index_bracket += + read; + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); + continue; } - }; - } - _ => { - error!( - "Error decoding SFTP Packet: {:?}", - e - ); - push_unsupported( - ReqId(u32::MAX), - &mut output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + _ => { + return Err(SunsetError::Bug.into()); + } + } } - }, - }; - } - _ => { - error!( - "Unhandled SftpHandleState {:?} in main loop", - self.state - ); - return Err(SunsetError::Bug.into()); - } - } + }; + } + _ => { + error!("Error decoding SFTP Packet: {:?}", e); + output_wrapper + .send_status( + ReqId(u32::MAX), + StatusCode::SSH_FX_OP_UNSUPPORTED, + "Error decoding SFTP Packet", + ) + .await?; + } + }, + }; buffer_in_lower_index_bracket = in_len - source.remaining(); } - }; + } } Ok(()) @@ -499,7 +494,8 @@ where ) -> SftpResult<()> { let (mut chan_in, chan_out) = stdio.split(); - let mut out_wrapper = OutputWrapper::new(buffer_out, chan_out); + let mut chan_out_wrapper = + SftpOutputChannelWrapper::new(buffer_out, chan_out); loop { let lr = chan_in.read(buffer_in).await?; trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); @@ -508,14 +504,13 @@ where return Err(SftpError::ClientDisconnected); } - self.process(&buffer_in[0..lr], &mut out_wrapper).await?; + self.process(&buffer_in[0..lr], &mut chan_out_wrapper).await?; } - // Ok(()) } async fn handle_general_request<'g>( file_server: &mut S, - output_wrapper: &mut OutputWrapper<'a, 'g>, + output_wrapper: &mut SftpOutputChannelWrapper<'a, 'g>, request: SftpPacket<'_>, ) -> Result<(), SftpError> where @@ -536,9 +531,7 @@ where &req_id, &response ); - response.encode_response(output_wrapper.get_mut_sink_ref())?; - // dbg!("PathInfo response encoded", &response); - output_wrapper.send_buffer().await?; + output_wrapper.send_packet(response).await?; } SftpPacket::Open(req_id, open) => { match file_server.open(open.filename.as_str()?, &open.attrs) { @@ -549,18 +542,13 @@ where handle: opaque_file_handle.into_file_handle(), }, ); - response - .encode_response(output_wrapper.get_mut_sink_ref())?; - output_wrapper.send_buffer().await?; + output_wrapper.send_packet(response).await?; } Err(status_code) => { error!("Open failed: {:?}", status_code); - push_general_failure( - req_id, - "", - output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status(req_id, StatusCode::SSH_FX_FAILURE, "") + .await?; } }; } @@ -573,34 +561,38 @@ where write.data.as_ref(), ) { Ok(_) => { - push_ok(req_id, output_wrapper.get_mut_sink_ref())?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status(req_id, StatusCode::SSH_FX_OK, "") + .await?; } Err(e) => { error!("SFTP write thrown: {:?}", e); - push_general_failure( - req_id, - "error writing", - output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status( + req_id, + StatusCode::SSH_FX_FAILURE, + "error writing", + ) + .await?; } }; } SftpPacket::Close(req_id, close) => { match file_server.close(&T::try_from(&close.handle)?) { Ok(_) => { - push_ok(req_id, output_wrapper.get_mut_sink_ref())?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status(req_id, StatusCode::SSH_FX_OK, "") + .await?; } Err(e) => { error!("SFTP Close thrown: {:?}", e); - push_general_failure( - req_id, - "", - output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status( + req_id, + StatusCode::SSH_FX_FAILURE, + "Could not Close the handle", + ) + .await?; } } } @@ -613,18 +605,13 @@ where handle: opaque_file_handle.into_file_handle(), }, ); - response - .encode_response(output_wrapper.get_mut_sink_ref())?; - output_wrapper.send_buffer().await?; + output_wrapper.send_packet(response).await?; } Err(status_code) => { error!("Open failed: {:?}", status_code); - push_general_failure( - req_id, - "", - output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status(req_id, StatusCode::SSH_FX_FAILURE, "") + .await?; } }; } @@ -632,9 +619,7 @@ where // TODO Implement the mechanism you are going to use to // handle the list of elements - let mut muting = 0; - - let mut dir_reply = DirReply::mock(req_id, &mut muting); + let mut dir_reply = DirReply::new(req_id, output_wrapper); match file_server .readdir(&T::try_from(&read_dir.handle)?, &mut dir_reply) @@ -644,11 +629,15 @@ where } Err(status_code) => { error!("Open failed: {:?}", status_code); - push_unsupported(req_id, output_wrapper.get_mut_sink_ref())?; - output_wrapper.send_buffer().await?; + // output_wrapper + // .push_status( + // req_id, + // StatusCode::SSH_FX_OP_UNSUPPORTED, + // "Error Reading Directory", + // ) + // .await?; } }; - debug!("final muting: {:?}", muting); error!("Unsupported Read Dir : {:?}", read_dir); // return Err(SftpError::NotSupported); // push_unsupported(ReqId(0), sink)?; @@ -663,13 +652,21 @@ where } // TODO Handle more long requests - /// Handles long request that do not fit in the buffers and stores a tracker + /// Some long request will not fit in the channel buffers. Such requests + /// will require to be handled differently. Gathering the data in and + /// processing it as we receive it in the channel in buffer. + /// + /// In the current approach a tracker is required to store the state of + /// the processing of such long requests. + /// + /// With an implementation that where able to hold the channel_in there might + /// be no need to keep this tracker. /// /// **WARNING:** Only `SSH_FXP_WRITE` has been implemented! /// async fn handle_ran_out<'g>( file_server: &mut S, - output_wrapper: &mut OutputWrapper<'a, 'g>, + output_wrapper: &mut SftpOutputChannelWrapper<'a, 'g>, source: &mut SftpSource<'_>, ) -> SftpResult> { let packet_type = source.peak_packet_type()?; @@ -712,12 +709,13 @@ where } Err(e) => { error!("SFTP write thrown: {:?}", e); - push_general_failure( - req_id, - "error writing ", - output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status( + req_id, + StatusCode::SSH_FX_FAILURE, + "error writing ", + ) + .await?; return Err(SftpError::FileServerError(e)); } }; @@ -733,117 +731,14 @@ where // Ok(()) } } - -struct OutputWrapper<'a, 'g> { - sink: SftpSink<'a>, - channel_out: ChanOut<'g>, -} - -impl<'a, 'g> OutputWrapper<'a, 'g> { - pub fn new(buffer: &'a mut [u8], channel_out: ChanOut<'g>) -> Self { - let sink = SftpSink::new(buffer); - OutputWrapper { channel_out, sink } - } - - pub fn reset(&mut self) { - self.sink.reset(); - } - - pub fn get_mut_sink_ref(&mut self) -> &mut SftpSink<'a> { - &mut self.sink - } - - // TODO Are we using this? - pub fn encode(&mut self, data: &T) -> SftpResult<()> - where - T: SSHEncode, - { - data.enc(&mut self.sink)?; - Ok(()) - } - // - - /// Finalizes (Prepends the packet length) and send the data in the - /// buffer by the subsystem channel out - pub async fn send_buffer(&mut self) -> SftpResult { - if self.sink.payload_len() == 0 { - debug!("No data to send in the SFTP sink"); - return Ok(0); - } - self.sink.finalize(); - let buffer = self.sink.used_slice(); - info!("Sending buffer: '{:?}'", buffer); - let written = self.channel_out.write(buffer).await?; - self.sink.reset(); - Ok(written) - } - - /// Send the data in the buffer by the subsystem channel out without - /// prepending the packet length to it. - /// - /// This is useful when an SFTP packet header has already being sent - /// or when the data requires an special treatment - pub async fn send_payload(&mut self) -> SftpResult { - let payload = self.sink.payload_slice(); - info!("Sending payload: '{:?}'", payload); - let written = self.channel_out.write(payload).await?; - self.sink.reset(); - Ok(written) - } - - pub fn finalize(&mut self) -> usize { - self.sink.finalize() - } -} - -#[inline] -fn push_ok(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireError> { - let response = SftpPacket::Status( - req_id, - Status { - code: StatusCode::SSH_FX_OK, - message: "".into(), - lang: "en-US".into(), - }, - ); - trace!("Pushing an OK status message: {:?}", response); - response.encode_response(sink)?; - Ok(()) -} - -#[inline] -fn push_unsupported( - req_id: ReqId, - sink: &mut SftpSink<'_>, -) -> Result<(), WireError> { - let response = SftpPacket::Status( - req_id, - Status { - code: StatusCode::SSH_FX_OP_UNSUPPORTED, - message: "Not implemented".into(), - lang: "en-US".into(), - }, - ); - debug!("Pushing a unsupported status message: {:?}", response); - response.encode_response(sink)?; - Ok(()) -} - -#[inline] -fn push_general_failure( - req_id: ReqId, - msg: &'static str, - sink: &mut SftpSink<'_>, -) -> Result<(), WireError> { - let response = SftpPacket::Status( - req_id, - Status { - code: StatusCode::SSH_FX_FAILURE, - message: msg.into(), - lang: "en-US".into(), - }, - ); - debug!("Pushing a general failure status message: {:?}", response); - response.encode_response(sink)?; - Ok(()) +/// Function to create an SFTP source and decode an SFTP packet from it +/// to avoid code duplication +fn create_sftp_source_and_packet( + buffer_in: &[u8], + buffer_in_lower_index_bracket: usize, +) -> (SftpSource<'_>, Result, WireError>) { + let mut source = SftpSource::new(&buffer_in[buffer_in_lower_index_bracket..]); + + let sftp_packet = SftpPacket::decode_request(&mut source); + (source, sftp_packet) } diff --git a/sftp/src/sftphandler/sftpoutputchannelwrapper.rs b/sftp/src/sftphandler/sftpoutputchannelwrapper.rs new file mode 100644 index 00000000..7710b7c3 --- /dev/null +++ b/sftp/src/sftphandler/sftpoutputchannelwrapper.rs @@ -0,0 +1,94 @@ +use crate::error::SftpResult; +use crate::proto::ReqId; +use crate::proto::SftpPacket; +use crate::proto::Status; +use crate::protocol::StatusCode; +use crate::server::SftpSink; + +use sunset::sshwire::WireError; +use sunset_async::ChanOut; + +use embedded_io_async::Write; +#[allow(unused_imports)] +use log::{debug, info, trace, warn}; + +/// Wrapper structure to handle SFTP output operations +/// +/// It wraps an SftpSink and a ChanOut to facilitate sending SFTP packets +/// even when they require multiple iterations +pub struct SftpOutputChannelWrapper<'a, 'g> { + sink: SftpSink<'a>, + channel_out: ChanOut<'g>, +} + +impl<'a, 'g> SftpOutputChannelWrapper<'a, 'g> { + /// Creates a new OutputWrapper + /// + /// This structure wraps an SftpSink and a ChanOut to facilitate + /// sending SFTP packets even when they require multiple steps + pub fn new(buffer: &'a mut [u8], channel_out: ChanOut<'g>) -> Self { + let sink = SftpSink::new(buffer); + SftpOutputChannelWrapper { channel_out, sink } + } + + /// Finalizes (Prepends the packet length) and send the data in the + /// buffer by the subsystem channel out + pub async fn send_buffer(&mut self) -> SftpResult { + if self.sink.payload_len() == 0 { + debug!("No data to send in the SFTP sink"); + return Ok(0); + } + self.sink.finalize(); + let buffer = self.sink.used_slice(); + info!("Sending buffer: '{:?}'", buffer); + let written = self.channel_out.write(buffer).await?; + self.sink.reset(); + Ok(written) + } + + /// Send the data in the buffer by the subsystem channel out without + /// prepending the packet length to it. + /// + /// This is useful when an SFTP packet header has already being sent + /// or when the data requires an special treatment + pub async fn send_payload(&mut self) -> SftpResult { + let payload = self.sink.payload_slice(); + info!("Sending payload: '{:?}'", payload); + let written = self.channel_out.write(payload).await?; + self.sink.reset(); + Ok(written) + } + + /// Push a status message into the channel out + pub async fn send_status( + &mut self, + req_id: ReqId, + status: StatusCode, + msg: &'static str, + ) -> Result<(), WireError> { + let response = SftpPacket::Status( + req_id, + Status { code: status, message: msg.into(), lang: "en-US".into() }, + ); + trace!("Pushing a status message: {:?}", response); + self.send_packet(response); + + Ok(()) + } + + /// Push an SFTP Packet into the channel out + pub async fn send_packet( + &mut self, + packet: SftpPacket<'_>, + ) -> Result<(), WireError> { + packet.encode_response(&mut self.sink)?; + self.send_buffer().await?; + Ok(()) + } + + pub async fn push(&mut self, item: &impl SSHEncode) -> Result<(), WireError> { + item.enc(&mut self.sink)?; + self.send_buffer().await?; + Ok(()) + } +} diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 895605c4..64c9a9ce 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,11 +1,13 @@ -use log::debug; - +use crate::error::SftpResult; +use crate::sftphandler::SftpOutputChannelWrapper; use crate::{ handles::OpaqueFileHandle, proto::{Attrs, Name, ReqId, StatusCode}, }; use core::marker::PhantomData; +use log::debug; + // use futures::executor::block_on; TODO Deal with the async nature of [`ChanOut`] /// Result used to store the result of an Sftp Operation @@ -148,6 +150,12 @@ impl<'g, 'a> ReadReply<'g, 'a> { pub fn reply(self, data: &[u8]) {} } +// TODO Implement correct Channel Out +pub struct ChanOut<'g, 'a> { + _phantom_g: PhantomData<&'g ()>, // 'g look what these might be ChanIO lifetime + _phantom_a: PhantomData<&'a ()>, // a' Why the second lifetime if ChanIO only needs one +} + // TODO Define this /// Dir Reply is the structure that will be "visiting" the [`SftpServer`] /// trait @@ -172,39 +180,47 @@ pub struct DirReply<'g, 'a> { /// Used during the req_id: ReqId, /// To test muting operations - muting: &'a mut u32, - chan: ChanOut<'g, 'a>, + chan_out: &'g mut SftpOutputChannelWrapper<'g>, } impl<'g, 'a> DirReply<'g, 'a> { /// I am faking a DirReply to prototype it - pub fn mock(req_id: ReqId, muting: &'a mut u32) -> Self { - DirReply { - chan: ChanOut { _phantom_g: PhantomData, _phantom_a: PhantomData }, - muting, - req_id, - } + // pub fn mock(req_id: ReqId, muting: &'a mut u32) -> Self { + // DirReply { + // chan_out: ChanOut { _phantom_g: PhantomData, _phantom_a: PhantomData }, + // // muting, + // req_id, + // } + // } + pub fn new( + req_id: ReqId, + chan_out_wrapper: &'g mut SftpOutputChannelWrapper<'g, 'g>, + ) -> Self { + DirReply { chan_out: chan_out_wrapper, req_id } } // TODO this will need to do async execution /// mocks sending an item via a stdio pub fn send_item(&mut self, data: &[u8]) { - *self.muting += 1; - debug!("Muted incremented {:?}. Got data: {:?}", self.muting, data); + // *self.muting += 1; + debug!("Send item: data = {:?}", data); } // TODO this will need to do async execution /// Must be call it first. Make this enforceable - pub fn send_header(&self, get_count: u32, get_encoded_len: u32) { + pub async fn send_header( + &mut self, + get_count: u32, + get_encoded_len: u32, + ) -> SftpResult<()> { debug!( "I will send the header here for request id {:?}: count = {:?}, length = {:?}", self.req_id, get_count, get_encoded_len ); + self.chan_out.push(&get_encoded_len).await?; + self.chan_out.push(&(104 as u8)).await?; + self.chan_out.push(&get_count).await?; + self.chan_out.send_payload().await?; + Ok(()) } } - -// TODO Implement correct Channel Out -pub struct ChanOut<'g, 'a> { - _phantom_g: PhantomData<&'g ()>, // 'g look what these might be ChanIO lifetime - _phantom_a: PhantomData<&'a ()>, // a' Why the second lifetime if ChanIO only needs one -} From 2e86051fcf42a0d620dc14b2bf3d952c64d58e67 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 5 Nov 2025 11:34:47 +1100 Subject: [PATCH 138/393] [skip ci] WIP: Adding Producer and Consumer to avoid mutable references writing to ChanOut [skip ci] WIP: Removing DirReplay wrong lifetime parameters --- demo/sftp/std/src/demosftpserver.rs | 2 +- demo/sftp/std/src/main.rs | 15 +- sftp/Cargo.toml | 2 + sftp/src/sftphandler/mod.rs | 1 + .../sftphandler/sftpoutputchannelhandler.rs | 162 ++++++++++++++++++ .../sftphandler/sftpoutputchannelwrapper.rs | 15 +- 6 files changed, 182 insertions(+), 15 deletions(-) create mode 100644 sftp/src/sftphandler/sftpoutputchannelhandler.rs diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 5c6818ed..8264ce4f 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -277,7 +277,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { fn readdir( &mut self, opaque_dir_handle: &DemoOpaqueFileHandle, - visitor: &mut DirReply<'_, '_>, + visitor: &mut DirReply<'_>, ) -> SftpOpResult<()> { debug!("read dir for {:?}", opaque_dir_handle); diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 4052cbb8..23c752de 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -157,15 +157,12 @@ impl DemoServer for StdDemo { "./demo/sftp/std/testing/out/".to_string(), ); - let mut sftp_handler = - SftpHandler::::new( - &mut file_server, - &mut incomplete_request_buffer, - ); - - sftp_handler - .process_loop(stdio, &mut buffer_in, &mut buffer_out) - .await?; + SftpHandler::::new( + &mut file_server, + &mut incomplete_request_buffer, + ) + .process_loop(stdio, &mut buffer_in, &mut buffer_out) + .await?; Ok::<_, Error>(()) } { diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 9a591f9f..86fb6d52 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -13,3 +13,5 @@ embedded-io-async = "0.6" num_enum = {version = "0.7.4"} paste = "1.0" log = "0.4" +embassy-sync = "0.7.2" +embassy-futures = "0.1.2" diff --git a/sftp/src/sftphandler/mod.rs b/sftp/src/sftphandler/mod.rs index fc32ff97..59cbb825 100644 --- a/sftp/src/sftphandler/mod.rs +++ b/sftp/src/sftphandler/mod.rs @@ -1,5 +1,6 @@ mod partialwriterequesttracker; mod sftphandler; +mod sftpoutputchannelhandler; mod sftpoutputchannelwrapper; pub use partialwriterequesttracker::PartialWriteRequestTracker; diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs new file mode 100644 index 00000000..e0fda789 --- /dev/null +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -0,0 +1,162 @@ +#![no_std] +use crate::error::SftpResult; +use crate::proto::{ReqId, SftpPacket, Status, StatusCode}; +use crate::server::SftpSink; + +use sunset_async::ChanOut; + +use embassy_sync::blocking_mutex::raw::RawMutex; +use embassy_sync::pipe::{Pipe, Reader as PipeReader, Writer as PipeWriter}; +use embedded_io_async::Write; + +use log::{debug, trace}; + +//// This is the beginning of a new idea: +/// I want to pass ref of an item where different methods in the sftphandler can +/// send data down the ChanOut. That would mutate the ChanOut (since it mutates on write) +/// and would violate the basic rule of only one mut borrow. +/// +/// To overcome this hurdle, I can use a two part solution related with a channel or a pipe. +/// +/// +/// Some notes: +/// +/// # sftpoutputchannelwrapper +/// Currently is a mutable entity. That causes issues since it needs to be mutated during loops. +/// ## first usage: +/// push SFTPEncode n times (composition) +// send_payload() +/// ## Second usage: +/// send_packet(SftpPacket) +/// ## last usage: +/// send_status('static str for messages) -> calls send_packet +/// # Alternative to avoid mutation: a channel or pipe +/// What would we put in the pipe? +/// ## 1st. composition: +/// We would create an SftpSink, add the SFTPEncode items and send it as a buffer. Maybe Len field eq to 0? +/// Maybe receive an SftpSink? +/// ## 2nd. SftpPacket +/// SftpSink, encode a packet, terminate it and send the SftpSink. len != 0 +/// ## 3rd. SftpPacket::Status +/// Compose the Status SftpPacket, Encode it in SftpSink, finalise it, send the buffer +// + +// enum AgentMsg { message to be sent} + +// static' RAW_PIPE = Pipe::::new(); + +pub struct SftpOutputPipe { + pipe: Pipe, + capacity: usize, +} + +/// M: SunsetRawMutex +impl SftpOutputPipe { + /// Creates an empty SftpOutputPipe. + /// The output channel will be consumed during the split call + /// + /// Usage: + /// + /// let output_pipe = SftpOutputPipe::::new(); + /// + fn new() -> Self { + SftpOutputPipe { pipe: Pipe::new(), capacity: N } + } + + /// Returns the inner pipe capacity. This method can be called after + /// split. + fn get_capacity(&self) -> usize { + self.capacity + } + + // TODO: Check if it panics when called twice + // TODO: Fix Doc links + /// Get a Consumer and Producer pair so the producer can send data to the + /// output channel without mutable borrows. + /// + /// The ['SftpOutputConsumer'] needs to be running to write data to the + /// ['ChanOut'] + /// + /// ## Lifetimes + /// The lifetime indicates that the lifetime of self, ChanOut and the + /// consumer and producer are the same. I chose this because if the ChanOut + /// is closed, there is no point on having a pipe outliving it. + fn split<'a>( + &'a mut self, + ssh_chan_out: ChanOut<'a>, + ) -> (SftpOutputConsumer<'a, M, N>, SftpOutputProducer<'a, M, N>) { + let (reader, writer) = self.pipe.split(); + (SftpOutputConsumer { reader, ssh_chan_out }, SftpOutputProducer { writer }) + } +} +pub struct SftpOutputConsumer<'a, M, const N: usize> +where + M: RawMutex, +{ + reader: PipeReader<'a, M, N>, + ssh_chan_out: ChanOut<'a>, +} +impl<'a, M, const N: usize> SftpOutputConsumer<'a, M, N> +where + M: RawMutex, +{ + /// Run it to start the piping + pub async fn receive_task(&mut self) -> SftpResult<()> { + let mut buf = [0u8; N]; + loop { + let rl = self.reader.read(&mut buf).await; + debug!("Read {} bytes", rl); + if rl > 0 { + self.ssh_chan_out.write_all(&buf[..rl]).await?; + } + } + } +} + +#[derive(Clone)] +pub struct SftpOutputProducer<'a, M, const N: usize> +where + M: RawMutex, +{ + writer: PipeWriter<'a, M, N>, +} +impl<'a, M, const N: usize> SftpOutputProducer<'a, M, N> +where + M: RawMutex, +{ + pub async fn send_payload(&self, sftp_sink: &SftpSink<'_>) -> SftpResult<()> { + let buf = sftp_sink.payload_slice(); + Self::send_buffer(&self.writer, &buf).await; + Ok(()) + } + + pub async fn send_status( + &self, + req_id: ReqId, + status: StatusCode, + msg: &'static str, + ) -> SftpResult<()> { + let response = SftpPacket::Status( + req_id, + Status { code: status, message: msg.into(), lang: "en-US".into() }, + ); + debug!("Pushing a status message: {:?}", response); + self.send_packet(&response).await?; + Ok(()) + } + + /// Push an SFTP Packet into the channel out + pub async fn send_packet(&self, packet: &SftpPacket<'_>) -> SftpResult<()> { + let mut buf = [0u8; N]; + let mut sink = SftpSink::new(&mut buf); + packet.encode_response(&mut sink); + debug!("Sending packet {:?}", packet); + Self::send_buffer(&self.writer, &buf).await; + Ok(()) + } + + async fn send_buffer(writer: &PipeWriter<'a, M, N>, buf: &[u8]) { + trace!("Sending buffer {:?}", buf); + writer.write(buf).await; + } +} diff --git a/sftp/src/sftphandler/sftpoutputchannelwrapper.rs b/sftp/src/sftphandler/sftpoutputchannelwrapper.rs index 7710b7c3..e9f7033f 100644 --- a/sftp/src/sftphandler/sftpoutputchannelwrapper.rs +++ b/sftp/src/sftphandler/sftpoutputchannelwrapper.rs @@ -5,6 +5,7 @@ use crate::proto::Status; use crate::protocol::StatusCode; use crate::server::SftpSink; +use sunset::sshwire::SSHEncode; use sunset::sshwire::WireError; use sunset_async::ChanOut; @@ -12,21 +13,25 @@ use embedded_io_async::Write; #[allow(unused_imports)] use log::{debug, info, trace, warn}; +pub struct BufferMsg { + pub data: [u8; N], + pub used: usize, +} /// Wrapper structure to handle SFTP output operations /// /// It wraps an SftpSink and a ChanOut to facilitate sending SFTP packets /// even when they require multiple iterations -pub struct SftpOutputChannelWrapper<'a, 'g> { - sink: SftpSink<'a>, +pub struct SftpOutputChannelWrapper<'g> { + sink: SftpSink<'g>, channel_out: ChanOut<'g>, } -impl<'a, 'g> SftpOutputChannelWrapper<'a, 'g> { +impl<'g> SftpOutputChannelWrapper<'g> { /// Creates a new OutputWrapper /// /// This structure wraps an SftpSink and a ChanOut to facilitate /// sending SFTP packets even when they require multiple steps - pub fn new(buffer: &'a mut [u8], channel_out: ChanOut<'g>) -> Self { + pub fn new(buffer: &'g mut [u8], channel_out: ChanOut<'g>) -> Self { let sink = SftpSink::new(buffer); SftpOutputChannelWrapper { channel_out, sink } } @@ -48,7 +53,7 @@ impl<'a, 'g> SftpOutputChannelWrapper<'a, 'g> { /// Send the data in the buffer by the subsystem channel out without /// prepending the packet length to it. - /// + /// /// This is useful when an SFTP packet header has already being sent /// or when the data requires an special treatment pub async fn send_payload(&mut self) -> SftpResult { From 49a915f879c41fb56ec2ac4a44d943c92f50487b Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 5 Nov 2025 15:00:38 +1100 Subject: [PATCH 139/393] [skip ci] WIP: Improved SftpSource readability --- sftp/src/sftpsource.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 7e1918e3..1a29a149 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -60,7 +60,12 @@ impl<'de> SftpSource<'de> { /// **Warning**: will only work in well formed packets, in other case /// the result will contain garbage pub(crate) fn peak_packet_type(&self) -> WireResult { - if self.buffer.len() < SFTP_FIELD_ID_INDEX + 1 { + if self.buffer.len() <= SFTP_FIELD_ID_INDEX { + error!( + "Peak packet type failed: buffer len <= SFTP_FIELD_ID_INDEX ( {:?} <= {:?})", + self.buffer.len(), + SFTP_FIELD_ID_INDEX + ); Err(WireError::RanOut) } else { Ok(SftpNum::from(self.buffer[SFTP_FIELD_ID_INDEX])) @@ -88,9 +93,15 @@ impl<'de> SftpSource<'de> { } /// Assuming that the buffer contains a [`proto::Write`] request packet initial - /// bytes and not its totality, extracts a partial version of the write request - /// and a Write request tracker to handle and a tracker to continue processing - /// subsequent portions of the request from a SftpSource + /// bytes and not its totality: + /// + /// **Returns**: + /// + /// - An [`OpaqueFileHandle`] to guide the Write operation, + /// - Request ID as [`ReqId`], + /// - Offset as [`u64`] + /// - Data in the buffer as [`BinString`] + /// - [`PartialWriteRequestTracker`] to handle subsequent portions of the request /// /// **Warning**: will only work in well formed write packets, in other case /// the result will contain garbage From 43399fa7794dc62d64a1f7f2f8512c30013cba93 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 5 Nov 2025 15:01:19 +1100 Subject: [PATCH 140/393] [skip ci] WIP: Removing excessive lifetimes to build successfully --- sftp/src/sftpserver.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 64c9a9ce..9e323ad6 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -78,7 +78,7 @@ where fn readdir( &mut self, opaque_dir_handle: &T, - reply: &mut DirReply<'_, '_>, + reply: &mut DirReply<'_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", @@ -176,14 +176,14 @@ pub struct ChanOut<'g, 'a> { /// c. Send each serialized `NameEntry`, excluding their length using /// the `send_item` method /// -pub struct DirReply<'g, 'a> { +pub struct DirReply<'g> { /// Used during the req_id: ReqId, /// To test muting operations chan_out: &'g mut SftpOutputChannelWrapper<'g>, } -impl<'g, 'a> DirReply<'g, 'a> { +impl<'g> DirReply<'g> { /// I am faking a DirReply to prototype it // pub fn mock(req_id: ReqId, muting: &'a mut u32) -> Self { // DirReply { @@ -194,7 +194,7 @@ impl<'g, 'a> DirReply<'g, 'a> { // } pub fn new( req_id: ReqId, - chan_out_wrapper: &'g mut SftpOutputChannelWrapper<'g, 'g>, + chan_out_wrapper: &'g mut SftpOutputChannelWrapper<'g>, ) -> Self { DirReply { chan_out: chan_out_wrapper, req_id } } From 4d10f36ba6b3949ad36abbdeff4fb857f3a5e810 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 5 Nov 2025 15:02:22 +1100 Subject: [PATCH 141/393] [skip ci] WIP: Improved readability in requestholder.rs [skip ci] WIP: Fixing the Pipe mutex to main crate SunsetRawMutex, solve send_packet bug and added debugging --- sftp/src/requestholder.rs | 40 ++++++------ .../sftphandler/sftpoutputchannelhandler.rs | 65 ++++++++----------- 2 files changed, 49 insertions(+), 56 deletions(-) diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs index e1427fc2..81dc9411 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/requestholder.rs @@ -108,7 +108,7 @@ impl<'a> RequestHolder<'a> { } /// Using the content of the `RequestHolder` tries to find a valid - /// SFTP request appending from slice into the internal buffer to + /// SFTP request appending bytes from slice_in into the internal buffer to /// form a valid request. /// /// Reset and increase the `appended()` counter. @@ -126,7 +126,7 @@ impl<'a> RequestHolder<'a> { /// - `Err(Bug)`: An unexpected condition arises pub fn try_append_for_valid_request( &mut self, - slice: &[u8], + slice_in: &[u8], ) -> RequestHolderResult<()> { debug!( "try_append_for_valid_request: self = {:?}\n\ @@ -134,7 +134,7 @@ impl<'a> RequestHolder<'a> { Length of slice to append from = {:?}", self, self.remaining_len(), - slice.len() + slice_in.len() ); if !self.busy { @@ -150,13 +150,13 @@ impl<'a> RequestHolder<'a> { self.appended = 0; // reset appended bytes counter // If we will not be able to read the SFTP packet ID we clearly need more data - if self.buffer_fill_index + slice.len() < proto::SFTP_FIELD_ID_INDEX { - self.try_append_slice(&slice)?; + if self.buffer_fill_index + slice_in.len() < proto::SFTP_FIELD_ID_INDEX { + self.try_append_slice(&slice_in)?; error!( "[Buffer fill index = {:?}] + [slice.len = {:?}] = {:?} < SFTP field id index = {:?}", self.buffer_fill_index, - slice.len(), - self.buffer_fill_index + slice.len(), + slice_in.len(), + self.buffer_fill_index + slice_in.len(), proto::SFTP_FIELD_ID_INDEX ); return Err(RequestHolderError::RanOut); @@ -175,15 +175,15 @@ impl<'a> RequestHolder<'a> { complete_to_id_index, proto::SFTP_FIELD_ID_INDEX ); - if complete_to_id_index > slice.len() { - self.try_append_slice(&slice)?; + if complete_to_id_index > slice_in.len() { + self.try_append_slice(&slice_in)?; error!( "The slice to include to the held fragment is too \ short to complete to id index. More data is required." ); return Err(RequestHolderError::RanOut); } else { - self.try_append_slice(&slice[..complete_to_id_index])?; + self.try_append_slice(&slice_in[..complete_to_id_index])?; }; } @@ -213,14 +213,16 @@ impl<'a> RequestHolder<'a> { + remaining_packet_len ); if remaining_packet_len <= self.remaining_len() { - // We have all the remaining packet bytes in the slice and fits in the buffer + // The remaining bytes would fill in the buffer - if (slice.len()) < (remaining_packet_len + self.appended()) { - self.try_append_slice(&slice[self.appended()..])?; + if (slice_in.len()) < (remaining_packet_len + self.appended()) { + // the slice_in does not contain all the remaining bytes + // We added them an request more + self.try_append_slice(&slice_in[self.appended()..])?; return Err(RequestHolderError::RanOut); } else { self.try_append_slice( - &slice[self.appended()..remaining_packet_len], + &slice_in[self.appended()..remaining_packet_len], )?; return Ok(()); } @@ -229,22 +231,22 @@ impl<'a> RequestHolder<'a> { // But they may not fit in the slice neither let start = self.appended(); - let end = self.remaining_len().min(slice.len() - self.appended()); + let end = self.remaining_len().min(slice_in.len() - self.appended()); debug!( "Will finally take the range: [{:?}..{:?}] from the slice [0..{:?}]", start, end, - slice.len() + slice_in.len() ); self.try_append_slice( - &slice[self.appended() - ..self.remaining_len().min(slice.len() - self.appended())], + &slice_in[self.appended() + ..self.remaining_len().min(slice_in.len() - self.appended())], )?; if self.is_full() { return Err(RequestHolderError::NoRoom); } else { - return Err(RequestHolderError::RanOut); + return Err(RequestHolderError::RanOut); // More bytes are needed to complete the Write request } } } diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index e0fda789..4d456b84 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -1,13 +1,12 @@ -#![no_std] use crate::error::SftpResult; use crate::proto::{ReqId, SftpPacket, Status, StatusCode}; use crate::server::SftpSink; use sunset_async::ChanOut; -use embassy_sync::blocking_mutex::raw::RawMutex; use embassy_sync::pipe::{Pipe, Reader as PipeReader, Writer as PipeWriter}; use embedded_io_async::Write; +use sunset_async::SunsetRawMutex; use log::{debug, trace}; @@ -43,29 +42,29 @@ use log::{debug, trace}; // enum AgentMsg { message to be sent} -// static' RAW_PIPE = Pipe::::new(); +// static' RAW_PIPE = Pipe::::new(); -pub struct SftpOutputPipe { - pipe: Pipe, +pub struct SftpOutputPipe { + pipe: Pipe, capacity: usize, } -/// M: SunsetRawMutex -impl SftpOutputPipe { +/// M: SunsetSunsetRawMutex +impl SftpOutputPipe { /// Creates an empty SftpOutputPipe. /// The output channel will be consumed during the split call /// /// Usage: /// - /// let output_pipe = SftpOutputPipe::::new(); + /// let output_pipe = SftpOutputPipe::::new(); /// - fn new() -> Self { + pub fn new() -> Self { SftpOutputPipe { pipe: Pipe::new(), capacity: N } } /// Returns the inner pipe capacity. This method can be called after /// split. - fn get_capacity(&self) -> usize { + pub fn get_capacity(&self) -> usize { self.capacity } @@ -81,31 +80,27 @@ impl SftpOutputPipe { /// The lifetime indicates that the lifetime of self, ChanOut and the /// consumer and producer are the same. I chose this because if the ChanOut /// is closed, there is no point on having a pipe outliving it. - fn split<'a>( + pub fn split<'a>( &'a mut self, ssh_chan_out: ChanOut<'a>, - ) -> (SftpOutputConsumer<'a, M, N>, SftpOutputProducer<'a, M, N>) { + ) -> (SftpOutputConsumer<'a, N>, SftpOutputProducer<'a, N>) { let (reader, writer) = self.pipe.split(); (SftpOutputConsumer { reader, ssh_chan_out }, SftpOutputProducer { writer }) } } -pub struct SftpOutputConsumer<'a, M, const N: usize> -where - M: RawMutex, -{ - reader: PipeReader<'a, M, N>, +pub struct SftpOutputConsumer<'a, const N: usize> { + reader: PipeReader<'a, SunsetRawMutex, N>, ssh_chan_out: ChanOut<'a>, } -impl<'a, M, const N: usize> SftpOutputConsumer<'a, M, N> -where - M: RawMutex, -{ +impl<'a, const N: usize> SftpOutputConsumer<'a, N> { /// Run it to start the piping pub async fn receive_task(&mut self) -> SftpResult<()> { + debug!("Running SftpOutout Consumer Reader task"); let mut buf = [0u8; N]; loop { let rl = self.reader.read(&mut buf).await; - debug!("Read {} bytes", rl); + debug!("Output Consumer Reader task: Reads {} bytes", rl); + debug!("Output Consumer Reader task: Bytes {:?}", &buf[..rl]); if rl > 0 { self.ssh_chan_out.write_all(&buf[..rl]).await?; } @@ -114,16 +109,10 @@ where } #[derive(Clone)] -pub struct SftpOutputProducer<'a, M, const N: usize> -where - M: RawMutex, -{ - writer: PipeWriter<'a, M, N>, +pub struct SftpOutputProducer<'a, const N: usize> { + writer: PipeWriter<'a, SunsetRawMutex, N>, } -impl<'a, M, const N: usize> SftpOutputProducer<'a, M, N> -where - M: RawMutex, -{ +impl<'a, const N: usize> SftpOutputProducer<'a, N> { pub async fn send_payload(&self, sftp_sink: &SftpSink<'_>) -> SftpResult<()> { let buf = sftp_sink.payload_slice(); Self::send_buffer(&self.writer, &buf).await; @@ -140,7 +129,7 @@ where req_id, Status { code: status, message: msg.into(), lang: "en-US".into() }, ); - debug!("Pushing a status message: {:?}", response); + debug!("Output Producer: Pushing a status message: {:?}", response); self.send_packet(&response).await?; Ok(()) } @@ -149,14 +138,16 @@ where pub async fn send_packet(&self, packet: &SftpPacket<'_>) -> SftpResult<()> { let mut buf = [0u8; N]; let mut sink = SftpSink::new(&mut buf); - packet.encode_response(&mut sink); - debug!("Sending packet {:?}", packet); - Self::send_buffer(&self.writer, &buf).await; + packet.encode_response(&mut sink)?; + debug!("Output Producer: Sending packet {:?}", packet); + sink.finalize(); + Self::send_buffer(&self.writer, &sink.used_slice()).await; Ok(()) } - async fn send_buffer(writer: &PipeWriter<'a, M, N>, buf: &[u8]) { - trace!("Sending buffer {:?}", buf); + async fn send_buffer(writer: &PipeWriter<'a, SunsetRawMutex, N>, buf: &[u8]) { + debug!("Output Producer: Sends {:?} bytes", buf.len()); + trace!("Output Producer: Sending buffer {:?}", buf); writer.write(buf).await; } } From e8414a8370b338b909679e8d41450f3f7b6d1963 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 5 Nov 2025 15:06:42 +1100 Subject: [PATCH 142/393] [skip ci] WIP: Fixing Bug where an error was thrown instead of trying to encode a packet --- sftp/src/sftphandler/sftphandler.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 71969f4b..7fafec0c 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -204,13 +204,11 @@ where continue; } RequestHolderError::NoRoom => { - error!( - "The request holder if full but the request in incomplete. \ - Consider increasing its size" - ); - // TODO react to this situation with an internal server error - return Err(SunsetError::NoRoom {}.into()); - } + warn!( + "The request holder is full but the request in is incomplete. \ + We will try to decode it" + ); + } _ => { error!( From d3d654ee61e87d028415b9fcf4bc6d31bf42b950 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 5 Nov 2025 15:10:31 +1100 Subject: [PATCH 143/393] [skip ci] WIP: Refactor sftphandler.rs and main.rs to accommodate the new Producer-Consumer for ChanOut It fixes the mutable borrow problems but I am getting the client disconnected with writing request: ``` Received message too long 1572864 Ensure the remote shell produces no output for non-interactive sessions. ``` [skip ci] WIP: Looking for the error. Made some small readability changes [skip ci] WIP: looking for traces explaining where the length [0,0,0,18] becames [18,0,0,0] Increasing verbosity level globally Still I cannot find where it is happening if it happens in Sunset SSH [skip ci] WIP: Tyding up: Removing deprecated parts of code ,commenting out others and adding comments --- demo/sftp/std/src/main.rs | 19 +- .../std/testing/test_long_write_requests.sh | 8 +- sftp/src/sftphandler/mod.rs | 2 - sftp/src/sftphandler/sftphandler.rs | 475 +++++++++--------- .../sftphandler/sftpoutputchannelhandler.rs | 36 +- .../sftphandler/sftpoutputchannelwrapper.rs | 99 ---- sftp/src/sftpserver.rs | 28 +- 7 files changed, 290 insertions(+), 377 deletions(-) delete mode 100644 sftp/src/sftphandler/sftpoutputchannelwrapper.rs diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 23c752de..ac251e37 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -89,7 +89,7 @@ impl DemoServer for StdDemo { async fn run(&self, serv: &SSHServer<'_>, mut common: DemoCommon) -> Result<()> { let chan_pipe = Channel::::new(); - let prog_loop_inner = async { + let ssh_loop_inner = async { loop { let mut ph = ProgressHolder::new(); let ev = serv.progress(&mut ph).await?; @@ -118,7 +118,7 @@ impl DemoServer for StdDemo { warn!( "request for subsystem '{}' not implemented: fail", a.command()? - ); + ); a.fail()?; } } @@ -130,9 +130,9 @@ impl DemoServer for StdDemo { Ok::<_, Error>(()) }; - let prog_loop = async { + let ssh_loop = async { info!("prog_loop started"); - if let Err(e) = prog_loop_inner.await { + if let Err(e) = ssh_loop_inner.await { warn!("Prog Loop Exited: {e:?}"); return Err(e); } @@ -148,8 +148,7 @@ impl DemoServer for StdDemo { // TODO Do some research to find reasonable default buffer lengths let mut buffer_in = [0u8; 512]; - let mut buffer_out = [0u8; 384]; - let mut incomplete_request_buffer = [0u8; 128]; + let mut incomplete_request_buffer = [0u8; 256]; match { let stdio = serv.stdio(ch).await?; @@ -157,11 +156,11 @@ impl DemoServer for StdDemo { "./demo/sftp/std/testing/out/".to_string(), ); - SftpHandler::::new( + SftpHandler::::new( &mut file_server, &mut incomplete_request_buffer, ) - .process_loop(stdio, &mut buffer_in, &mut buffer_out) + .process_loop(stdio, &mut buffer_in) .await?; Ok::<_, Error>(()) @@ -179,7 +178,7 @@ impl DemoServer for StdDemo { Ok::<_, Error>(()) }; - let selected = select(prog_loop, sftp_loop).await; + let selected = select(ssh_loop, sftp_loop).await; match selected { embassy_futures::select::Either::First(res) => { warn!("prog_loop finished: {:?}", res); @@ -206,7 +205,7 @@ async fn listen( #[embassy_executor::main] async fn main(spawner: Spawner) { env_logger::builder() - .filter_level(log::LevelFilter::Debug) + .filter_level(log::LevelFilter::Trace) .filter_module("sunset::runner", log::LevelFilter::Info) .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) diff --git a/demo/sftp/std/testing/test_long_write_requests.sh b/demo/sftp/std/testing/test_long_write_requests.sh index a0104e17..532edd32 100755 --- a/demo/sftp/std/testing/test_long_write_requests.sh +++ b/demo/sftp/std/testing/test_long_write_requests.sh @@ -5,17 +5,17 @@ REMOTE_HOST="192.168.69.2" REMOTE_USER="any" # Define test files -FILES=("100MB_random" "1024MB_random") +FILES=("100MB_random") # Generate random data files echo "Generating random data files..." dd if=/dev/random bs=1048576 count=100 of=./100MB_random 2>/dev/null -dd if=/dev/random bs=1048576 count=1024 of=./1024MB_random 2>/dev/null +# dd if=/dev/random bs=1048576 count=1024 of=./1024MB_random 2>/dev/null echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." # Upload all files -sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} -vvv << EOF $(printf 'put ./%s\n' "${FILES[@]}") bye EOF @@ -33,6 +33,6 @@ for file in "${FILES[@]}"; do done echo "Cleaning up local files..." -rm -f ./*_random ./out/*_random +rm -f -r ./*_random ./out/*_random echo "Upload test completed." \ No newline at end of file diff --git a/sftp/src/sftphandler/mod.rs b/sftp/src/sftphandler/mod.rs index 59cbb825..c7660cd1 100644 --- a/sftp/src/sftphandler/mod.rs +++ b/sftp/src/sftphandler/mod.rs @@ -1,8 +1,6 @@ mod partialwriterequesttracker; mod sftphandler; mod sftpoutputchannelhandler; -mod sftpoutputchannelwrapper; pub use partialwriterequesttracker::PartialWriteRequestTracker; pub use sftphandler::SftpHandler; -pub use sftpoutputchannelwrapper::SftpOutputChannelWrapper; diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 7fafec0c..37bd0a23 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -7,12 +7,14 @@ use crate::proto::{ SftpPacket, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; -use crate::server::DirReply; use crate::sftperror::SftpResult; -use crate::sftphandler::sftpoutputchannelwrapper::SftpOutputChannelWrapper; +use crate::sftphandler::sftpoutputchannelhandler::{ + SftpOutputPipe, SftpOutputProducer, +}; use crate::sftpserver::SftpServer; use crate::sftpsource::SftpSource; +use embassy_futures::select::select; use sunset::Error as SunsetError; use sunset::sshwire::{SSHSource, WireError}; use sunset_async::ChanInOut; @@ -88,7 +90,7 @@ enum FragmentedRequestState { /// It will delegate request to an [`crate::sftpserver::SftpServer`] /// implemented by the library /// user taking into account the local system details. -pub struct SftpHandler<'a, T, S> +pub struct SftpHandler<'a, T, S, const BUFFER_OUT_SIZE: usize> where T: OpaqueFileHandle, S: SftpServer<'a, T>, @@ -107,7 +109,7 @@ where incomplete_request_holder: RequestHolder<'a>, } -impl<'a, T, S> SftpHandler<'a, T, S> +impl<'a, T, S, const BUFFER_OUT_SIZE: usize> SftpHandler<'a, T, S, BUFFER_OUT_SIZE> where T: OpaqueFileHandle, S: SftpServer<'a, T>, @@ -143,10 +145,10 @@ where /// /// **Returns**: A result containing the number of bytes used in /// `buffer_out` - async fn process<'g>( + async fn process( &mut self, buffer_in: &[u8], - output_wrapper: &mut SftpOutputChannelWrapper<'a, 'g>, + output_producer: &SftpOutputProducer<'_, BUFFER_OUT_SIZE>, ) -> SftpResult<()> { let in_len = buffer_in.len(); let mut buffer_in_lower_index_bracket = 0; @@ -171,203 +173,191 @@ where match &self.state { // There is a fragmented request in process of processing - SftpHandleState::Fragmented(fragment_case) => { - match fragment_case { - FragmentedRequestState::ProcessingClippedRequest => { - if let Err(e) = self - .incomplete_request_holder - .try_append_for_valid_request( - &buffer_in[buffer_in_lower_index_bracket..], - ) - { - match e { - RequestHolderError::RanOut => { - warn!( - "There was not enough bytes in the buffer_in. \ - We will continue adding bytes" - ); - buffer_in_lower_index_bracket += self - .incomplete_request_holder - .appended(); - continue; - } - RequestHolderError::WireError( - WireError::RanOut, - ) => { - warn!( - "WIRE ERROR: There was not enough bytes in the buffer_in. \ - We will continue adding bytes" - ); - buffer_in_lower_index_bracket += self - .incomplete_request_holder - .appended(); - continue; - } - RequestHolderError::NoRoom => { + SftpHandleState::Fragmented(fragment_case) => match fragment_case { + FragmentedRequestState::ProcessingClippedRequest => { + if let Err(e) = self + .incomplete_request_holder + .try_append_for_valid_request( + &buffer_in[buffer_in_lower_index_bracket..], + ) + { + match e { + RequestHolderError::RanOut => { + warn!( + "There was not enough bytes in the buffer_in. \ + We will continue adding bytes" + ); + buffer_in_lower_index_bracket += + self.incomplete_request_holder.appended(); + continue; + } + RequestHolderError::WireError(WireError::RanOut) => { + warn!( + "WIRE ERROR: There was not enough bytes in the buffer_in. \ + We will continue adding bytes" + ); + buffer_in_lower_index_bracket += + self.incomplete_request_holder.appended(); + continue; + } + RequestHolderError::NoRoom => { warn!( "The request holder is full but the request in is incomplete. \ We will try to decode it" ); } - _ => { - error!( - "Unhandled error completing incomplete request {:?}", - e, - ); - return Err(SunsetError::Bug.into()); - } + _ => { + error!( + "Unhandled error completing incomplete request {:?}", + e, + ); + return Err(SunsetError::Bug.into()); } - } else { - debug!( - "Incomplete request holder completed the request!" - ); } + } else { + debug!( + "Incomplete request holder completed the request!" + ); + } - let used = self.incomplete_request_holder.appended(); - buffer_in_lower_index_bracket += used; + let used = self.incomplete_request_holder.appended(); + buffer_in_lower_index_bracket += used; - let mut source = SftpSource::new( - &self.incomplete_request_holder.try_get_ref()?, - ); - trace!("Internal Source Content: {:?}", source); - - match SftpPacket::decode_request(&mut source) { - Ok(request) => { - Self::handle_general_request( - &mut self.file_server, - output_wrapper, - request, - ) - .await?; - self.incomplete_request_holder.reset(); - self.state = SftpHandleState::Idle; - } - Err(e) => match e { - WireError::RanOut => match Self::handle_ran_out( - &mut self.file_server, - output_wrapper, - &mut source, - ) - .await - { - Ok(holder) => { - self.partial_write_request_tracker = - Some(holder); - self.incomplete_request_holder.reset(); - self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); + let mut source = SftpSource::new( + &self.incomplete_request_holder.try_get_ref()?, + ); + trace!("Internal Source Content: {:?}", source); + + match SftpPacket::decode_request(&mut source) { + Ok(request) => { + Self::handle_general_request( + &mut self.file_server, + output_producer, + request, + ) + .await?; + self.incomplete_request_holder.reset(); + self.state = SftpHandleState::Idle; + } + Err(e) => match e { + WireError::RanOut => match Self::handle_ran_out( + &mut self.file_server, + output_producer, + &mut source, + ) + .await + { + Ok(holder) => { + self.partial_write_request_tracker = + Some(holder); + self.incomplete_request_holder.reset(); + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); + } + Err(e) => match e { + _ => { + error!( + "handle_ran_out finished with error: {:?}", + e + ); + return Err(SunsetError::Bug.into()); } - Err(e) => match e { - _ => { - error!( - "handle_ran_out finished with error: {:?}", - e - ); - return Err(SunsetError::Bug.into()); - } - }, }, - WireError::NoRoom => { - error!("Not enough space to fit the request") - } - _ => { - error!( - "Unhandled error decoding assembled packet: {:?}", - e - ); - return Err(WireError::PacketWrong.into()); - } }, - } + WireError::NoRoom => { + error!("Not enough space to fit the request") + } + _ => { + error!( + "Unhandled error decoding assembled packet: {:?}", + e + ); + return Err(WireError::PacketWrong.into()); + } + }, } - FragmentedRequestState::ProcessingLongRequest => { - let mut source = SftpSource::new( - &buffer_in[buffer_in_lower_index_bracket..], + } + FragmentedRequestState::ProcessingLongRequest => { + let mut source = SftpSource::new( + &buffer_in[buffer_in_lower_index_bracket..], + ); + trace!("Source content: {:?}", source); + + let mut write_tracker = if let Some(wt) = + self.partial_write_request_tracker.take() + { + wt + } else { + error!( + "BUG: FragmentedRequestState::ProcessingLongRequest cannot take the write tracker" ); - trace!("Source content: {:?}", source); - - let mut write_tracker = if let Some(wt) = - self.partial_write_request_tracker.take() - { - wt - } else { - error!( - "BUG: FragmentedRequestState::ProcessingLongRequest cannot take the write tracker" - ); - return Err(SunsetError::Bug.into()); - }; + return Err(SunsetError::Bug.into()); + }; - let opaque_handle = - write_tracker.get_opaque_file_handle(); + let opaque_handle = write_tracker.get_opaque_file_handle(); - let usable_data = source - .remaining() - .min(write_tracker.get_remain_data_len() as usize); + let usable_data = source + .remaining() + .min(write_tracker.get_remain_data_len() as usize); - let data_segment = // Fails!! - source.dec_as_binstring(usable_data)?; + let data_segment = source.dec_as_binstring(usable_data)?; - let data_segment_len = u32::try_from( - data_segment.0.len(), - ) + let data_segment_len = u32::try_from(data_segment.0.len()) .map_err(|e| { - error!("Error casting data segment len to u32: {e}"); - SunsetError::Bug - })?; - let current_write_offset = - write_tracker.get_remain_data_offset(); - write_tracker.update_remaining_after_partial_write( - data_segment_len, - ); + error!("Error casting data segment len to u32: {e}"); + SunsetError::Bug + })?; + let current_write_offset = + write_tracker.get_remain_data_offset(); + write_tracker + .update_remaining_after_partial_write(data_segment_len); - debug!( - "Processing successive chunks of a long write packet. \ - Writing : opaque_handle = {:?}, write_offset = {:?}, \ - data_segment = {:?}, data remaining = {:?}", - opaque_handle, - current_write_offset, - data_segment, - write_tracker.get_remain_data_len() - ); + debug!( + "Processing successive chunks of a long write packet. \ + Writing : opaque_handle = {:?}, write_offset = {:?}, \ + data_segment = {:?}, data remaining = {:?}", + opaque_handle, + current_write_offset, + data_segment, + write_tracker.get_remain_data_len() + ); - match self.file_server.write( - &opaque_handle, - current_write_offset, - data_segment.as_ref(), - ) { - Ok(_) => { - if write_tracker.get_remain_data_len() > 0 { - self.partial_write_request_tracker = - Some(write_tracker); - } else { - output_wrapper - .send_status( - write_tracker.get_req_id(), - StatusCode::SSH_FX_OK, - "", - ) - .await?; - info!("Finished multi part Write Request"); - self.state = SftpHandleState::Idle; - } - } - Err(e) => { - error!("SFTP write thrown: {:?}", e); - output_wrapper + match self.file_server.write( + &opaque_handle, + current_write_offset, + data_segment.as_ref(), + ) { + Ok(_) => { + if write_tracker.get_remain_data_len() > 0 { + self.partial_write_request_tracker = + Some(write_tracker); + } else { + output_producer .send_status( write_tracker.get_req_id(), - StatusCode::SSH_FX_FAILURE, - "error writing", + StatusCode::SSH_FX_OK, + "", ) .await?; + info!("Finished multi part Write Request"); self.state = SftpHandleState::Idle; } - }; - buffer_in_lower_index_bracket = - in_len - source.remaining(); - } + } + Err(e) => { + error!("SFTP write thrown: {:?}", e); + output_producer + .send_status( + write_tracker.get_req_id(), + StatusCode::SSH_FX_FAILURE, + "error writing", + ) + .await?; + self.state = SftpHandleState::Idle; + } + }; + buffer_in_lower_index_bracket = in_len - source.remaining(); } - } + }, SftpHandleState::Initializing => { let (source, sftp_packet) = create_sftp_source_and_packet( @@ -383,7 +373,7 @@ where version: SFTP_VERSION, }); - output_wrapper.send_packet(version).await?; + output_producer.send_packet(&version).await?; self.state = SftpHandleState::Idle; } _ => { @@ -414,7 +404,7 @@ where Ok(request) => { Self::handle_general_request( &mut self.file_server, - output_wrapper, + output_producer, request, ) .await?; @@ -428,7 +418,7 @@ where match Self::handle_ran_out( &mut self.file_server, - output_wrapper, + output_producer, &mut source, ) .await @@ -437,7 +427,7 @@ where self.partial_write_request_tracker = Some(holder); self.state = - SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) + SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) } Err(e) => { error!("Error handle_ran_out"); @@ -445,11 +435,12 @@ where SftpError::WireError( WireError::RanOut, ) => { + debug!(""); let read = self.incomplete_request_holder - .try_hold( - &buffer_in - [buffer_in_lower_index_bracket..], - )?; + .try_hold( + &buffer_in + [buffer_in_lower_index_bracket..], + )?; buffer_in_lower_index_bracket += read; self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); @@ -464,7 +455,7 @@ where } _ => { error!("Error decoding SFTP Packet: {:?}", e); - output_wrapper + output_producer .send_status( ReqId(u32::MAX), StatusCode::SSH_FX_OP_UNSUPPORTED, @@ -484,31 +475,49 @@ where /// WIP: A loop that will process all the request from stdio until /// an EOF is received - pub async fn process_loop<'c>( + pub async fn process_loop( &mut self, - stdio: ChanInOut<'c>, + stdio: ChanInOut<'a>, buffer_in: &mut [u8], - buffer_out: &'a mut [u8], + // buffer_out: &'a mut [u8], ) -> SftpResult<()> { let (mut chan_in, chan_out) = stdio.split(); - let mut chan_out_wrapper = - SftpOutputChannelWrapper::new(buffer_out, chan_out); - loop { - let lr = chan_in.read(buffer_in).await?; - trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); - if lr == 0 { - debug!("client disconnected"); - return Err(SftpError::ClientDisconnected); - } + let mut sftp_output_pipe = SftpOutputPipe::::new(); + + let (mut output_consumer, output_producer) = + sftp_output_pipe.split(chan_out); + + let output_consumer_loop = output_consumer.receive_task(); + + let processing_loop = async { + loop { + let lr = chan_in.read(buffer_in).await?; + trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); + if lr == 0 { + debug!("client disconnected"); + return Err(SftpError::ClientDisconnected); + } - self.process(&buffer_in[0..lr], &mut chan_out_wrapper).await?; + self.process(&buffer_in[0..lr], &output_producer).await?; + } + SftpResult::Ok(()) + }; + match select(processing_loop, output_consumer_loop).await { + embassy_futures::select::Either::First(r) => { + debug!("Processing returned: {:?}", r); + r + } + embassy_futures::select::Either::Second(r) => { + warn!("Output consumer returned: {:?}", r); + r + } } } - async fn handle_general_request<'g>( + async fn handle_general_request( file_server: &mut S, - output_wrapper: &mut SftpOutputChannelWrapper<'a, 'g>, + output_producer: &SftpOutputProducer<'_, BUFFER_OUT_SIZE>, request: SftpPacket<'_>, ) -> Result<(), SftpError> where @@ -529,7 +538,7 @@ where &req_id, &response ); - output_wrapper.send_packet(response).await?; + output_producer.send_packet(&response).await?; } SftpPacket::Open(req_id, open) => { match file_server.open(open.filename.as_str()?, &open.attrs) { @@ -540,11 +549,11 @@ where handle: opaque_file_handle.into_file_handle(), }, ); - output_wrapper.send_packet(response).await?; + output_producer.send_packet(&response).await?; } Err(status_code) => { error!("Open failed: {:?}", status_code); - output_wrapper + output_producer .send_status(req_id, StatusCode::SSH_FX_FAILURE, "") .await?; } @@ -559,13 +568,13 @@ where write.data.as_ref(), ) { Ok(_) => { - output_wrapper + output_producer .send_status(req_id, StatusCode::SSH_FX_OK, "") .await?; } Err(e) => { error!("SFTP write thrown: {:?}", e); - output_wrapper + output_producer .send_status( req_id, StatusCode::SSH_FX_FAILURE, @@ -578,13 +587,13 @@ where SftpPacket::Close(req_id, close) => { match file_server.close(&T::try_from(&close.handle)?) { Ok(_) => { - output_wrapper + output_producer .send_status(req_id, StatusCode::SSH_FX_OK, "") .await?; } Err(e) => { error!("SFTP Close thrown: {:?}", e); - output_wrapper + output_producer .send_status( req_id, StatusCode::SSH_FX_FAILURE, @@ -603,11 +612,11 @@ where handle: opaque_file_handle.into_file_handle(), }, ); - output_wrapper.send_packet(response).await?; + output_producer.send_packet(&response).await?; } Err(status_code) => { error!("Open failed: {:?}", status_code); - output_wrapper + output_producer .send_status(req_id, StatusCode::SSH_FX_FAILURE, "") .await?; } @@ -617,26 +626,26 @@ where // TODO Implement the mechanism you are going to use to // handle the list of elements - let mut dir_reply = DirReply::new(req_id, output_wrapper); - - match file_server - .readdir(&T::try_from(&read_dir.handle)?, &mut dir_reply) - { - Ok(_) => { - todo!("Dance starts here"); - } - Err(status_code) => { - error!("Open failed: {:?}", status_code); - // output_wrapper - // .push_status( - // req_id, - // StatusCode::SSH_FX_OP_UNSUPPORTED, - // "Error Reading Directory", - // ) - // .await?; - } - }; - error!("Unsupported Read Dir : {:?}", read_dir); + // let mut dir_reply = DirReply::new(req_id, output_wrapper); + + // match file_server + // .readdir(&T::try_from(&read_dir.handle)?, &mut dir_reply) + // { + // Ok(_) => { + // todo!("Dance starts here"); + // } + // Err(status_code) => { + // error!("Open failed: {:?}", status_code); + // // output_wrapper + // // .push_status( + // // req_id, + // // StatusCode::SSH_FX_OP_UNSUPPORTED, + // // "Error Reading Directory", + // // ) + // // .await?; + // } + // }; + // error!("Unsupported Read Dir : {:?}", read_dir); // return Err(SftpError::NotSupported); // push_unsupported(ReqId(0), sink)?; } @@ -662,11 +671,12 @@ where /// /// **WARNING:** Only `SSH_FXP_WRITE` has been implemented! /// - async fn handle_ran_out<'g>( + async fn handle_ran_out( file_server: &mut S, - output_wrapper: &mut SftpOutputChannelWrapper<'a, 'g>, + output_producer: &SftpOutputProducer<'_, BUFFER_OUT_SIZE>, source: &mut SftpSource<'_>, ) -> SftpResult> { + debug!("Handing Ran out"); let packet_type = source.peak_packet_type()?; match packet_type { SftpNum::SSH_FXP_WRITE => { @@ -707,7 +717,7 @@ where } Err(e) => { error!("SFTP write thrown: {:?}", e); - output_wrapper + output_producer .send_status( req_id, StatusCode::SSH_FX_FAILURE, @@ -735,6 +745,11 @@ fn create_sftp_source_and_packet( buffer_in: &[u8], buffer_in_lower_index_bracket: usize, ) -> (SftpSource<'_>, Result, WireError>) { + debug!( + "Creating a source: lower_index_bracket = {:?}, buffer_len = {:?}", + buffer_in_lower_index_bracket, + buffer_in.len() + ); let mut source = SftpSource::new(&buffer_in[buffer_in_lower_index_bracket..]); let sftp_packet = SftpPacket::decode_request(&mut source); diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 4d456b84..69421ba9 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -8,7 +8,7 @@ use embassy_sync::pipe::{Pipe, Reader as PipeReader, Writer as PipeWriter}; use embedded_io_async::Write; use sunset_async::SunsetRawMutex; -use log::{debug, trace}; +use log::{debug, error, trace}; //// This is the beginning of a new idea: /// I want to pass ref of an item where different methods in the sftphandler can @@ -46,7 +46,6 @@ use log::{debug, trace}; pub struct SftpOutputPipe { pipe: Pipe, - capacity: usize, } /// M: SunsetSunsetRawMutex @@ -59,22 +58,15 @@ impl SftpOutputPipe { /// let output_pipe = SftpOutputPipe::::new(); /// pub fn new() -> Self { - SftpOutputPipe { pipe: Pipe::new(), capacity: N } - } - - /// Returns the inner pipe capacity. This method can be called after - /// split. - pub fn get_capacity(&self) -> usize { - self.capacity + SftpOutputPipe { pipe: Pipe::new() } } // TODO: Check if it panics when called twice - // TODO: Fix Doc links /// Get a Consumer and Producer pair so the producer can send data to the /// output channel without mutable borrows. /// - /// The ['SftpOutputConsumer'] needs to be running to write data to the - /// ['ChanOut'] + /// The [`SftpOutputConsumer`] needs to be running to write data to the + /// [`ChanOut`] /// /// ## Lifetimes /// The lifetime indicates that the lifetime of self, ChanOut and the @@ -88,10 +80,14 @@ impl SftpOutputPipe { (SftpOutputConsumer { reader, ssh_chan_out }, SftpOutputProducer { writer }) } } + +/// Consumer that takes ownership of [`ChanOut`]. It pipes the data received +/// from a [`PipeReader`] into the channel pub struct SftpOutputConsumer<'a, const N: usize> { reader: PipeReader<'a, SunsetRawMutex, N>, ssh_chan_out: ChanOut<'a>, } + impl<'a, const N: usize> SftpOutputConsumer<'a, N> { /// Run it to start the piping pub async fn receive_task(&mut self) -> SftpResult<()> { @@ -99,26 +95,35 @@ impl<'a, const N: usize> SftpOutputConsumer<'a, N> { let mut buf = [0u8; N]; loop { let rl = self.reader.read(&mut buf).await; - debug!("Output Consumer Reader task: Reads {} bytes", rl); - debug!("Output Consumer Reader task: Bytes {:?}", &buf[..rl]); + debug!("Output Consumer: Reads {} bytes", rl); + debug!("Output Consumer: Bytes {:?}", &buf[..rl]); if rl > 0 { self.ssh_chan_out.write_all(&buf[..rl]).await?; + } else { + error!("Output Consumer: Empty array received"); } } } } +/// Producer used to send data to a [`ChanOut`] without the restrictions +/// of mutable borrows #[derive(Clone)] pub struct SftpOutputProducer<'a, const N: usize> { writer: PipeWriter<'a, SunsetRawMutex, N>, } impl<'a, const N: usize> SftpOutputProducer<'a, N> { + /// Send the data encoded in the provided [`SftpSink`] without including + /// the size. + /// + /// Use this when you are sending chunks of data after a valid header pub async fn send_payload(&self, sftp_sink: &SftpSink<'_>) -> SftpResult<()> { let buf = sftp_sink.payload_slice(); Self::send_buffer(&self.writer, &buf).await; Ok(()) } + /// Simplifies the task of sending a status response to the client. pub async fn send_status( &self, req_id: ReqId, @@ -134,7 +139,7 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { Ok(()) } - /// Push an SFTP Packet into the channel out + /// Sends an SFTP Packet into the channel out, including the length field pub async fn send_packet(&self, packet: &SftpPacket<'_>) -> SftpResult<()> { let mut buf = [0u8; N]; let mut sink = SftpSink::new(&mut buf); @@ -145,6 +150,7 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { Ok(()) } + /// Internal associated method to log the writes to the pipe async fn send_buffer(writer: &PipeWriter<'a, SunsetRawMutex, N>, buf: &[u8]) { debug!("Output Producer: Sends {:?} bytes", buf.len()); trace!("Output Producer: Sending buffer {:?}", buf); diff --git a/sftp/src/sftphandler/sftpoutputchannelwrapper.rs b/sftp/src/sftphandler/sftpoutputchannelwrapper.rs deleted file mode 100644 index e9f7033f..00000000 --- a/sftp/src/sftphandler/sftpoutputchannelwrapper.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::error::SftpResult; -use crate::proto::ReqId; -use crate::proto::SftpPacket; -use crate::proto::Status; -use crate::protocol::StatusCode; -use crate::server::SftpSink; - -use sunset::sshwire::SSHEncode; -use sunset::sshwire::WireError; -use sunset_async::ChanOut; - -use embedded_io_async::Write; -#[allow(unused_imports)] -use log::{debug, info, trace, warn}; - -pub struct BufferMsg { - pub data: [u8; N], - pub used: usize, -} -/// Wrapper structure to handle SFTP output operations -/// -/// It wraps an SftpSink and a ChanOut to facilitate sending SFTP packets -/// even when they require multiple iterations -pub struct SftpOutputChannelWrapper<'g> { - sink: SftpSink<'g>, - channel_out: ChanOut<'g>, -} - -impl<'g> SftpOutputChannelWrapper<'g> { - /// Creates a new OutputWrapper - /// - /// This structure wraps an SftpSink and a ChanOut to facilitate - /// sending SFTP packets even when they require multiple steps - pub fn new(buffer: &'g mut [u8], channel_out: ChanOut<'g>) -> Self { - let sink = SftpSink::new(buffer); - SftpOutputChannelWrapper { channel_out, sink } - } - - /// Finalizes (Prepends the packet length) and send the data in the - /// buffer by the subsystem channel out - pub async fn send_buffer(&mut self) -> SftpResult { - if self.sink.payload_len() == 0 { - debug!("No data to send in the SFTP sink"); - return Ok(0); - } - self.sink.finalize(); - let buffer = self.sink.used_slice(); - info!("Sending buffer: '{:?}'", buffer); - let written = self.channel_out.write(buffer).await?; - self.sink.reset(); - Ok(written) - } - - /// Send the data in the buffer by the subsystem channel out without - /// prepending the packet length to it. - /// - /// This is useful when an SFTP packet header has already being sent - /// or when the data requires an special treatment - pub async fn send_payload(&mut self) -> SftpResult { - let payload = self.sink.payload_slice(); - info!("Sending payload: '{:?}'", payload); - let written = self.channel_out.write(payload).await?; - self.sink.reset(); - Ok(written) - } - - /// Push a status message into the channel out - pub async fn send_status( - &mut self, - req_id: ReqId, - status: StatusCode, - msg: &'static str, - ) -> Result<(), WireError> { - let response = SftpPacket::Status( - req_id, - Status { code: status, message: msg.into(), lang: "en-US".into() }, - ); - trace!("Pushing a status message: {:?}", response); - self.send_packet(response); - - Ok(()) - } - - /// Push an SFTP Packet into the channel out - pub async fn send_packet( - &mut self, - packet: SftpPacket<'_>, - ) -> Result<(), WireError> { - packet.encode_response(&mut self.sink)?; - self.send_buffer().await?; - Ok(()) - } - - pub async fn push(&mut self, item: &impl SSHEncode) -> Result<(), WireError> { - item.enc(&mut self.sink)?; - self.send_buffer().await?; - Ok(()) - } -} diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 9e323ad6..a783100a 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,5 +1,4 @@ use crate::error::SftpResult; -use crate::sftphandler::SftpOutputChannelWrapper; use crate::{ handles::OpaqueFileHandle, proto::{Attrs, Name, ReqId, StatusCode}, @@ -179,24 +178,19 @@ pub struct ChanOut<'g, 'a> { pub struct DirReply<'g> { /// Used during the req_id: ReqId, - /// To test muting operations - chan_out: &'g mut SftpOutputChannelWrapper<'g>, + + _phantom_g: PhantomData<&'g ()>, + // /// To test muting operations + // chan_out: &'g mut SftpOutputChannelWrapper<'g>, } impl<'g> DirReply<'g> { - /// I am faking a DirReply to prototype it - // pub fn mock(req_id: ReqId, muting: &'a mut u32) -> Self { - // DirReply { - // chan_out: ChanOut { _phantom_g: PhantomData, _phantom_a: PhantomData }, - // // muting, - // req_id, - // } - // } pub fn new( req_id: ReqId, - chan_out_wrapper: &'g mut SftpOutputChannelWrapper<'g>, + // chan_out_wrapper: &'g mut SftpOutputChannelWrapper<'g>, ) -> Self { - DirReply { chan_out: chan_out_wrapper, req_id } + // DirReply { chan_out: chan_out_wrapper, req_id } + DirReply { req_id, _phantom_g: PhantomData } } // TODO this will need to do async execution @@ -217,10 +211,10 @@ impl<'g> DirReply<'g> { "I will send the header here for request id {:?}: count = {:?}, length = {:?}", self.req_id, get_count, get_encoded_len ); - self.chan_out.push(&get_encoded_len).await?; - self.chan_out.push(&(104 as u8)).await?; - self.chan_out.push(&get_count).await?; - self.chan_out.send_payload().await?; + // self.chan_out.push(&get_encoded_len).await?; + // self.chan_out.push(&(104 as u8)).await?; + // self.chan_out.push(&get_count).await?; + // self.chan_out.send_payload().await?; Ok(()) } } From cc464621d941e3f5310e15b81f75fc4884e75185 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 6 Nov 2025 17:02:31 +1100 Subject: [PATCH 144/393] [skip ci] WIP: write request fails but will be parked for now I am going to focus on listing folder files --- demo/sftp/std/src/main.rs | 13 ++++++++++--- sftp/src/sftphandler/sftpoutputchannelhandler.rs | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index ac251e37..74f1b34b 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -205,14 +205,21 @@ async fn listen( #[embassy_executor::main] async fn main(spawner: Spawner) { env_logger::builder() - .filter_level(log::LevelFilter::Trace) + .filter_level(log::LevelFilter::Info) .filter_module("sunset::runner", log::LevelFilter::Info) + // .filter_module("sunset::runner", log::LevelFilter::Trace) + // .filter_module("sunset::channel", log::LevelFilter::Trace) + .filter_module( + "sunset_sftp::sftphandler::sftpoutputchannelhandler", + log::LevelFilter::Trace, + ) .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) - .filter_module("sunset::traffic", log::LevelFilter::Info) + // .filter_module("sunset::traffic", log::LevelFilter::Trace) .filter_module("sunset::encrypt", log::LevelFilter::Info) .filter_module("sunset::conn", log::LevelFilter::Info) - .filter_module("sunset::kex", log::LevelFilter::Info) + // .filter_module("sunset::kex", log::LevelFilter::Info) + .filter_module("sunset::kex", log::LevelFilter::Trace) .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) .filter_module("async_io", log::LevelFilter::Info) .filter_module("polling", log::LevelFilter::Info) diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 69421ba9..54c07994 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -96,9 +96,9 @@ impl<'a, const N: usize> SftpOutputConsumer<'a, N> { loop { let rl = self.reader.read(&mut buf).await; debug!("Output Consumer: Reads {} bytes", rl); - debug!("Output Consumer: Bytes {:?}", &buf[..rl]); if rl > 0 { self.ssh_chan_out.write_all(&buf[..rl]).await?; + debug!("Output Consumer: Bytes written {:?}", &buf[..rl]); } else { error!("Output Consumer: Empty array received"); } From f5167a31cfec2acbe9669154872f14bec0b90040 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 6 Nov 2025 18:22:41 +1100 Subject: [PATCH 145/393] [skip ci] WIP: Some issues in DemoSftpServer DirEntriesCollection but ready to make the SftpServer trait functions async The length calculation seems erroneous but I am more worried about the async trait [skip ci] WIP: Progress with readdir. Needs tyding up Issue with pipe Looks like the number of sent items in the pipe does not match received items. I am going to add a send/receive total bytes counter. It might also be the cause of the write errors [skip ci] WIP: Cargo lock update long forgotten. Apologies --- Cargo.lock | 24 +++--- demo/sftp/std/src/demosftpserver.rs | 52 ++++++------- demo/sftp/std/src/main.rs | 9 ++- sftp/src/sftphandler/mod.rs | 1 + sftp/src/sftphandler/sftphandler.rs | 41 +++++----- .../sftphandler/sftpoutputchannelhandler.rs | 13 +++- sftp/src/sftpserver.rs | 74 +++++++++++++------ 7 files changed, 128 insertions(+), 86 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de7fac7f..500e36e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -805,9 +805,9 @@ dependencies = [ [[package]] name = "embassy-futures" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f878075b9794c1e4ac788c95b728f26aa6366d32eeb10c7051389f898f7d067" +checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01" [[package]] name = "embassy-hal-internal" @@ -934,15 +934,15 @@ dependencies = [ [[package]] name = "embassy-sync" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef1a8a1ea892f9b656de0295532ac5d8067e9830d49ec75076291fd6066b136" +checksum = "73974a3edbd0bd286759b3d483540f0ebef705919a5f56f4fc7709066f71689b" dependencies = [ "cfg-if", "critical-section", "embedded-io-async", + "futures-core", "futures-sink", - "futures-util", "heapless", ] @@ -2777,7 +2777,7 @@ name = "sunset-async" version = "0.3.0" dependencies = [ "embassy-futures", - "embassy-sync 0.7.0", + "embassy-sync 0.7.2", "embedded-io-async", "log", "portable-atomic", @@ -2794,7 +2794,7 @@ dependencies = [ "embassy-futures", "embassy-net", "embassy-net-driver", - "embassy-sync 0.7.0", + "embassy-sync 0.7.2", "embassy-time", "embedded-io-async", "heapless", @@ -2824,7 +2824,7 @@ dependencies = [ "embassy-net", "embassy-net-wiznet", "embassy-rp", - "embassy-sync 0.7.0", + "embassy-sync 0.7.2", "embassy-time", "embassy-usb", "embassy-usb-driver", @@ -2859,7 +2859,7 @@ dependencies = [ "embassy-futures", "embassy-net", "embassy-net-tuntap", - "embassy-sync 0.7.0", + "embassy-sync 0.7.2", "embassy-time", "embedded-io-async", "env_logger", @@ -2885,7 +2885,7 @@ dependencies = [ "embassy-futures", "embassy-net", "embassy-net-tuntap", - "embassy-sync 0.7.0", + "embassy-sync 0.7.2", "embassy-time", "embedded-io-async", "env_logger", @@ -2917,6 +2917,8 @@ dependencies = [ name = "sunset-sftp" version = "0.1.1" dependencies = [ + "embassy-futures", + "embassy-sync 0.7.2", "embedded-io-async", "log", "num_enum 0.7.4", @@ -2941,7 +2943,7 @@ dependencies = [ "argh", "critical-section", "embassy-futures", - "embassy-sync 0.7.0", + "embassy-sync 0.7.2", "embedded-io-adapters", "embedded-io-async", "futures", diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 8264ce4f..b53c5849 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -274,17 +274,17 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } } - fn readdir( + async fn readdir( &mut self, opaque_dir_handle: &DemoOpaqueFileHandle, - visitor: &mut DirReply<'_>, + reply: &DirReply<'_, N>, ) -> SftpOpResult<()> { debug!("read dir for {:?}", opaque_dir_handle); if let PrivatePathHandle::Directory(dir) = self .handles_manager .get_private_as_ref(opaque_dir_handle) - .ok_or(StatusCode::SSH_FX_FAILURE)? + .ok_or(StatusCode::SSH_FX_NO_SUCH_FILE)? { let path_str = dir.path.clone(); debug!("opaque handle found in handles manager: {:?}", path_str); @@ -301,13 +301,9 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { let name_entry_collection = DirEntriesCollection::new(dir_iterator); - visitor.send_header( - name_entry_collection.get_count()?, - name_entry_collection.get_encoded_len()?, - ); + name_entry_collection.send_header(reply).await?; - name_entry_collection - .for_each_encoded(|data: &[u8]| visitor.send_item(data))?; + name_entry_collection.send_entries(reply).await?; } else { error!("the path is not a directory = {:?}", dir_path); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); @@ -316,13 +312,19 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { error!("Could not find the directory for {:?}", opaque_dir_handle); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); } - - error!("What is the return that we are looking for?"); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + Ok(()) } } // TODO Add this to SFTP library only available with std as a global helper +/// This is a helper structure to make ReadDir into something somehow +/// digestible by [`DirReply`] +/// +/// WIP: Not stable. It has know issues and most likely it's methods will change +/// +/// BUG: It does not count properly the number of bytes +/// +/// BUG: It does not include longname and that may be an issue #[derive(Debug)] pub struct DirEntriesCollection { /// Number of elements @@ -396,21 +398,21 @@ impl DirEntriesCollection { ext_count: None, } } -} -impl DirEntriesResponseHelpers for DirEntriesCollection { - fn get_count(&self) -> SftpOpResult { - Ok(self.count) - } - - fn get_encoded_len(&self) -> SftpOpResult { - Ok(self.encoded_length) + pub async fn send_header( + &self, + reply: &DirReply<'_, N>, + ) -> SftpOpResult<()> { + reply.send_header(self.count, self.encoded_length).await.map_err(|e| { + debug!("Could not send header {e:?}"); + StatusCode::SSH_FX_FAILURE + }) } - fn for_each_encoded(&self, mut writer: F) -> SftpOpResult<()> - where - F: FnMut(&[u8]) -> (), - { + pub async fn send_entries( + &self, + reply: &DirReply<'_, N>, + ) -> SftpOpResult<()> { for entry in &self.entries { let filename = entry.path().to_string_lossy().into_owned(); let attrs = Self::get_attrs_or_empty(entry.metadata()); @@ -426,7 +428,7 @@ impl DirEntriesResponseHelpers for DirEntriesCollection { debug!("WireError: {:?}", err); StatusCode::SSH_FX_FAILURE })?; - writer(sftp_sink.payload_slice()); + reply.send_item(sftp_sink.payload_slice()).await; } Ok(()) } diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 74f1b34b..22baf1ec 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -211,14 +211,17 @@ async fn main(spawner: Spawner) { // .filter_module("sunset::channel", log::LevelFilter::Trace) .filter_module( "sunset_sftp::sftphandler::sftpoutputchannelhandler", - log::LevelFilter::Trace, + log::LevelFilter::Debug, ) .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) - // .filter_module("sunset::traffic", log::LevelFilter::Trace) + .filter_module( + "sunset_demo_sftp_std::demosftpserver", + log::LevelFilter::Debug, + ) + .filter_module("sunset_sftp::sftpserver", log::LevelFilter::Debug) .filter_module("sunset::encrypt", log::LevelFilter::Info) .filter_module("sunset::conn", log::LevelFilter::Info) - // .filter_module("sunset::kex", log::LevelFilter::Info) .filter_module("sunset::kex", log::LevelFilter::Trace) .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) .filter_module("async_io", log::LevelFilter::Info) diff --git a/sftp/src/sftphandler/mod.rs b/sftp/src/sftphandler/mod.rs index c7660cd1..6122c204 100644 --- a/sftp/src/sftphandler/mod.rs +++ b/sftp/src/sftphandler/mod.rs @@ -4,3 +4,4 @@ mod sftpoutputchannelhandler; pub use partialwriterequesttracker::PartialWriteRequestTracker; pub use sftphandler::SftpHandler; +pub use sftpoutputchannelhandler::SftpOutputProducer; diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 37bd0a23..bbd34c13 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -7,6 +7,7 @@ use crate::proto::{ SftpPacket, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; +use crate::server::{DirReply, SftpOpResult}; use crate::sftperror::SftpResult; use crate::sftphandler::sftpoutputchannelhandler::{ SftpOutputPipe, SftpOutputProducer, @@ -626,33 +627,27 @@ where // TODO Implement the mechanism you are going to use to // handle the list of elements - // let mut dir_reply = DirReply::new(req_id, output_wrapper); - - // match file_server - // .readdir(&T::try_from(&read_dir.handle)?, &mut dir_reply) - // { - // Ok(_) => { - // todo!("Dance starts here"); - // } - // Err(status_code) => { - // error!("Open failed: {:?}", status_code); - // // output_wrapper - // // .push_status( - // // req_id, - // // StatusCode::SSH_FX_OP_UNSUPPORTED, - // // "Error Reading Directory", - // // ) - // // .await?; - // } - // }; - // error!("Unsupported Read Dir : {:?}", read_dir); - // return Err(SftpError::NotSupported); - // push_unsupported(ReqId(0), sink)?; + let mut dir_reply = DirReply::new(req_id, output_producer); + + match file_server + .readdir(&T::try_from(&read_dir.handle)?, &mut dir_reply) + .await + { + Ok(_) => {} + Err(status) => { + error!("Open failed: {:?}", status); + + output_producer.send_status( + req_id, + status, + "Error Reading Directory", + ); + } + }; } _ => { error!("Unsupported request type: {:?}", request); return Err(SftpError::NotSupported); - // push_unsupported(ReqId(0), sink)?; } } Ok(()) diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 54c07994..78d160cc 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -113,7 +113,16 @@ pub struct SftpOutputProducer<'a, const N: usize> { writer: PipeWriter<'a, SunsetRawMutex, N>, } impl<'a, const N: usize> SftpOutputProducer<'a, N> { - /// Send the data encoded in the provided [`SftpSink`] without including + /// Sends the data encoded in the provided [`SftpSink`] without including + /// the size. + /// + /// Use this when you are sending chunks of data after a valid header + pub async fn send_data(&self, buf: &[u8]) -> SftpResult<()> { + Self::send_buffer(&self.writer, &buf).await; + Ok(()) + } + + /// Sends the data encoded in the provided [`SftpSink`] without including /// the size. /// /// Use this when you are sending chunks of data after a valid header @@ -139,7 +148,7 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { Ok(()) } - /// Sends an SFTP Packet into the channel out, including the length field + /// Sends a SFTP Packet into the channel out, including the length field pub async fn send_packet(&self, packet: &SftpPacket<'_>) -> SftpResult<()> { let mut buf = [0u8; N]; let mut sink = SftpSink::new(&mut buf); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index a783100a..017677fa 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,4 +1,6 @@ use crate::error::SftpResult; +use crate::server::SftpSink; +use crate::sftphandler::SftpOutputProducer; use crate::{ handles::OpaqueFileHandle, proto::{Attrs, Name, ReqId, StatusCode}, @@ -6,6 +8,7 @@ use crate::{ use core::marker::PhantomData; use log::debug; +use sunset::sshwire::SSHEncode; // use futures::executor::block_on; TODO Deal with the async nature of [`ChanOut`] @@ -74,10 +77,10 @@ where /// Reads the list of items in a directory #[allow(unused_variables)] - fn readdir( + async fn readdir( &mut self, opaque_dir_handle: &T, - reply: &mut DirReply<'_>, + reply: &DirReply<'_, N>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", @@ -86,6 +89,15 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } + // async fn readdir( + // &mut self, + // opaque_dir_handle: &T, + // reply: &DirReply<'_, N>, + // ) -> SftpOpResult<()> { + // log::error!("SftpServer ReadDir operation not defined"); + // Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + // } + /// Provides the real path of the directory specified fn realpath(&mut self, dir: &str) -> SftpOpResult> { log::error!("SftpServer RealPath operation not defined: dir = {:?}", dir); @@ -127,6 +139,18 @@ pub trait DirEntriesResponseHelpers { { Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } + + // /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter + // /// were a [`NameEntry`] has been encoded. + // /// + // /// + // #[allow(unused_variables)] + // fn encoded_iter( + // &self, + // writer: F, + // ) -> impl Iterator> { + // Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + // } } // TODO Define this @@ -175,35 +199,31 @@ pub struct ChanOut<'g, 'a> { /// c. Send each serialized `NameEntry`, excluding their length using /// the `send_item` method /// -pub struct DirReply<'g> { +pub struct DirReply<'g, const N: usize> { /// Used during the req_id: ReqId, - _phantom_g: PhantomData<&'g ()>, + // _phantom_g: PhantomData<&'g ()>, // /// To test muting operations - // chan_out: &'g mut SftpOutputChannelWrapper<'g>, + chan_out: &'g SftpOutputProducer<'g, N>, } -impl<'g> DirReply<'g> { - pub fn new( - req_id: ReqId, - // chan_out_wrapper: &'g mut SftpOutputChannelWrapper<'g>, - ) -> Self { +impl<'g, const N: usize> DirReply<'g, N> { + pub fn new(req_id: ReqId, chan_out: &'g SftpOutputProducer<'g, N>) -> Self { // DirReply { chan_out: chan_out_wrapper, req_id } - DirReply { req_id, _phantom_g: PhantomData } + DirReply { req_id, chan_out } } - // TODO this will need to do async execution - /// mocks sending an item via a stdio - pub fn send_item(&mut self, data: &[u8]) { + /// Sends an item to the client + pub async fn send_item(&self, data: &[u8]) { // *self.muting += 1; - debug!("Send item: data = {:?}", data); + debug!("Sending item: len = {:?}, content = {:?}", data.len(), data); + self.chan_out.send_data(data).await; } - // TODO this will need to do async execution - /// Must be call it first. Make this enforceable + /// Sends the header to the client. TODO Make this enforceable pub async fn send_header( - &mut self, + &self, get_count: u32, get_encoded_len: u32, ) -> SftpResult<()> { @@ -211,10 +231,20 @@ impl<'g> DirReply<'g> { "I will send the header here for request id {:?}: count = {:?}, length = {:?}", self.req_id, get_count, get_encoded_len ); - // self.chan_out.push(&get_encoded_len).await?; - // self.chan_out.push(&(104 as u8)).await?; - // self.chan_out.push(&get_count).await?; - // self.chan_out.send_payload().await?; + let mut s = [0u8; N]; + let mut sink = SftpSink::new(&mut s); + + get_encoded_len.enc(&mut sink)?; + 104u8.enc(&mut sink)?; + self.req_id.enc(&mut sink)?; + get_count.enc(&mut sink)?; + let payload = sink.payload_slice(); + debug!( + "Sending header: len = {:?}, content = {:?}", + payload.len(), + payload + ); + self.chan_out.send_data(sink.payload_slice()).await?; Ok(()) } } From e5a21bb44aac0a3eb73aa7fa315f20c7ec566804 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 7 Nov 2025 12:02:31 +1100 Subject: [PATCH 146/393] [skip ci] WIP: FIX read operation Looping to write all bytes. The counters confirm that there is no missing bytes as they match. Tested with a 100MB writing operation: ./test_long_write_requests.sh I am confident that this the write issue is solved --- demo/sftp/std/src/demosftpserver.rs | 12 +- sftp/src/lib.rs | 2 +- sftp/src/sftphandler/sftphandler.rs | 8 +- .../sftphandler/sftpoutputchannelhandler.rs | 97 ++++++++------ sftp/src/sftpserver.rs | 124 +++++++++--------- 5 files changed, 125 insertions(+), 118 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index b53c5849..3d412cc7 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -7,10 +7,7 @@ use sunset::sshwire::SSHEncode; use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; use sunset_sftp::protocol::constants::MAX_NAME_ENTRY_SIZE; use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; -use sunset_sftp::server::{ - DirEntriesResponseHelpers, DirReply, ReadReply, SftpOpResult, SftpServer, - SftpSink, -}; +use sunset_sftp::server::{DirReply, ReadReply, SftpOpResult, SftpServer, SftpSink}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; @@ -425,10 +422,13 @@ impl DirEntriesCollection { let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; let mut sftp_sink = SftpSink::new(&mut buffer); name_entry.enc(&mut sftp_sink).map_err(|err| { - debug!("WireError: {:?}", err); + error!("WireError: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; + reply.send_item(sftp_sink.payload_slice()).await.map_err(|err| { + error!("SftpError: {:?}", err); StatusCode::SSH_FX_FAILURE })?; - reply.send_item(sftp_sink.payload_slice()).await; } Ok(()) } diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 0a9b81b0..accfcab2 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -69,7 +69,7 @@ pub use sftphandler::SftpHandler; /// Structures and types used to add the details for the target system pub mod server { - pub use crate::sftpserver::DirEntriesResponseHelpers; + // pub use crate::sftpserver::DirEntriesResponseHelpers; pub use crate::sftpserver::DirReply; pub use crate::sftpserver::ReadReply; pub use crate::sftpserver::SftpOpResult; diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index bbd34c13..d370cddc 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -637,11 +637,9 @@ where Err(status) => { error!("Open failed: {:?}", status); - output_producer.send_status( - req_id, - status, - "Error Reading Directory", - ); + output_producer + .send_status(req_id, status, "Error Reading Directory") + .await?; } }; } diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 78d160cc..085e86f8 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -2,6 +2,7 @@ use crate::error::SftpResult; use crate::proto::{ReqId, SftpPacket, Status, StatusCode}; use crate::server::SftpSink; +use embassy_sync::mutex::Mutex; use sunset_async::ChanOut; use embassy_sync::pipe::{Pipe, Reader as PipeReader, Writer as PipeWriter}; @@ -10,42 +11,12 @@ use sunset_async::SunsetRawMutex; use log::{debug, error, trace}; -//// This is the beginning of a new idea: -/// I want to pass ref of an item where different methods in the sftphandler can -/// send data down the ChanOut. That would mutate the ChanOut (since it mutates on write) -/// and would violate the basic rule of only one mut borrow. -/// -/// To overcome this hurdle, I can use a two part solution related with a channel or a pipe. -/// -/// -/// Some notes: -/// -/// # sftpoutputchannelwrapper -/// Currently is a mutable entity. That causes issues since it needs to be mutated during loops. -/// ## first usage: -/// push SFTPEncode n times (composition) -// send_payload() -/// ## Second usage: -/// send_packet(SftpPacket) -/// ## last usage: -/// send_status('static str for messages) -> calls send_packet -/// # Alternative to avoid mutation: a channel or pipe -/// What would we put in the pipe? -/// ## 1st. composition: -/// We would create an SftpSink, add the SFTPEncode items and send it as a buffer. Maybe Len field eq to 0? -/// Maybe receive an SftpSink? -/// ## 2nd. SftpPacket -/// SftpSink, encode a packet, terminate it and send the SftpSink. len != 0 -/// ## 3rd. SftpPacket::Status -/// Compose the Status SftpPacket, Encode it in SftpSink, finalise it, send the buffer -// - -// enum AgentMsg { message to be sent} - -// static' RAW_PIPE = Pipe::::new(); +type CounterMutex = Mutex; pub struct SftpOutputPipe { pipe: Pipe, + counter_send: CounterMutex, + counter_recv: CounterMutex, } /// M: SunsetSunsetRawMutex @@ -58,7 +29,11 @@ impl SftpOutputPipe { /// let output_pipe = SftpOutputPipe::::new(); /// pub fn new() -> Self { - SftpOutputPipe { pipe: Pipe::new() } + SftpOutputPipe { + pipe: Pipe::new(), + counter_send: Mutex::::new(0), + counter_recv: Mutex::::new(0), + } } // TODO: Check if it panics when called twice @@ -77,7 +52,10 @@ impl SftpOutputPipe { ssh_chan_out: ChanOut<'a>, ) -> (SftpOutputConsumer<'a, N>, SftpOutputProducer<'a, N>) { let (reader, writer) = self.pipe.split(); - (SftpOutputConsumer { reader, ssh_chan_out }, SftpOutputProducer { writer }) + ( + SftpOutputConsumer { reader, ssh_chan_out, counter: &self.counter_recv }, + SftpOutputProducer { writer, counter: &self.counter_send }, + ) } } @@ -86,6 +64,7 @@ impl SftpOutputPipe { pub struct SftpOutputConsumer<'a, const N: usize> { reader: PipeReader<'a, SunsetRawMutex, N>, ssh_chan_out: ChanOut<'a>, + counter: &'a CounterMutex, } impl<'a, const N: usize> SftpOutputConsumer<'a, N> { @@ -95,10 +74,18 @@ impl<'a, const N: usize> SftpOutputConsumer<'a, N> { let mut buf = [0u8; N]; loop { let rl = self.reader.read(&mut buf).await; - debug!("Output Consumer: Reads {} bytes", rl); + let mut total = 0; + { + let mut lock = self.counter.lock().await; + *lock += rl; + total = *lock; + } + + debug!("Output Consumer: Reads {rl} bytes. Total {total}"); if rl > 0 { self.ssh_chan_out.write_all(&buf[..rl]).await?; - debug!("Output Consumer: Bytes written {:?}", &buf[..rl]); + debug!("Output Consumer: Written {:?} bytes ", &buf[..rl].len()); + trace!("Output Consumer: Bytes written {:?}", &buf[..rl]); } else { error!("Output Consumer: Empty array received"); } @@ -111,6 +98,7 @@ impl<'a, const N: usize> SftpOutputConsumer<'a, N> { #[derive(Clone)] pub struct SftpOutputProducer<'a, const N: usize> { writer: PipeWriter<'a, SunsetRawMutex, N>, + counter: &'a CounterMutex, } impl<'a, const N: usize> SftpOutputProducer<'a, N> { /// Sends the data encoded in the provided [`SftpSink`] without including @@ -118,7 +106,7 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { /// /// Use this when you are sending chunks of data after a valid header pub async fn send_data(&self, buf: &[u8]) -> SftpResult<()> { - Self::send_buffer(&self.writer, &buf).await; + Self::send_buffer(&self.writer, &buf, &self.counter).await; Ok(()) } @@ -128,7 +116,7 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { /// Use this when you are sending chunks of data after a valid header pub async fn send_payload(&self, sftp_sink: &SftpSink<'_>) -> SftpResult<()> { let buf = sftp_sink.payload_slice(); - Self::send_buffer(&self.writer, &buf).await; + Self::send_buffer(&self.writer, &buf, &self.counter).await; Ok(()) } @@ -155,14 +143,37 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { packet.encode_response(&mut sink)?; debug!("Output Producer: Sending packet {:?}", packet); sink.finalize(); - Self::send_buffer(&self.writer, &sink.used_slice()).await; + Self::send_buffer(&self.writer, &sink.used_slice(), &self.counter).await; Ok(()) } /// Internal associated method to log the writes to the pipe - async fn send_buffer(writer: &PipeWriter<'a, SunsetRawMutex, N>, buf: &[u8]) { - debug!("Output Producer: Sends {:?} bytes", buf.len()); + async fn send_buffer( + writer: &PipeWriter<'a, SunsetRawMutex, N>, + buf: &[u8], + counter: &CounterMutex, + ) { + let mut total = 0; + { + let mut lock = counter.lock().await; + *lock += buf.len(); + total = *lock; + } + + debug!("Output Producer: Sends {:?} bytes. Total {total}", buf.len()); trace!("Output Producer: Sending buffer {:?}", buf); - writer.write(buf).await; + + // writer.write_all(buf); // ??? error[E0596]: cannot borrow `*writer` as mutable, as it is behind a `&` reference + + let mut buf = buf; + loop { + if buf.len() == 0 { + break; + } + trace!("Sending buffer {:?}", buf); + + let bytes_sent = writer.write(&buf).await; + buf = &buf[bytes_sent..]; + } } } diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 017677fa..21e2ea64 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -7,7 +7,7 @@ use crate::{ }; use core::marker::PhantomData; -use log::debug; +use log::{debug, trace}; use sunset::sshwire::SSHEncode; // use futures::executor::block_on; TODO Deal with the async nature of [`ChanOut`] @@ -105,53 +105,53 @@ where } } -/// This trait is an standardized way to interact with an iterator or collection of Directory entries -/// that need to be sent via an SSH_FXP_READDIR SFTP response to a client. -/// -/// It uses is expected when implementing an [`SftpServer`] TODO Future trait WIP -pub trait DirEntriesResponseHelpers { - /// returns the number of directory entries. - /// Used for the `SSH_FXP_READDIR` response field `count` - /// as specified in [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) - fn get_count(&self) -> SftpOpResult { - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) - } - - /// Returns the total encoded length in bytes for all directory entries. - /// Used for the `SSH_FXP_READDIR` general response field `length` - /// as part of the [General Packet Format](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-3) - /// - /// This represents the sum of all [`NameEntry`] structures when encoded - /// into [`SftpSink`] format. The length is to be pre-computed by - /// encoding each entry and summing the [`SftpSink::payload_len()`] values. - fn get_encoded_len(&self) -> SftpOpResult { - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) - } - - /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter - /// were a [`NameEntry`] has been encoded. - /// - /// - #[allow(unused_variables)] - fn for_each_encoded(&self, writer: F) -> SftpOpResult<()> - where - F: FnMut(&[u8]) -> (), - { - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) - } - - // /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter - // /// were a [`NameEntry`] has been encoded. - // /// - // /// - // #[allow(unused_variables)] - // fn encoded_iter( - // &self, - // writer: F, - // ) -> impl Iterator> { - // Err(StatusCode::SSH_FX_OP_UNSUPPORTED) - // } -} +// /// This trait is an standardized way to interact with an iterator or collection of Directory entries +// /// that need to be sent via an SSH_FXP_READDIR SFTP response to a client. +// /// +// /// It uses is expected when implementing an [`SftpServer`] TODO Future trait WIP +// pub trait DirEntriesResponseHelpers { +// /// returns the number of directory entries. +// /// Used for the `SSH_FXP_READDIR` response field `count` +// /// as specified in [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) +// fn get_count(&self) -> SftpOpResult { +// Err(StatusCode::SSH_FX_OP_UNSUPPORTED) +// } + +// /// Returns the total encoded length in bytes for all directory entries. +// /// Used for the `SSH_FXP_READDIR` general response field `length` +// /// as part of the [General Packet Format](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-3) +// /// +// /// This represents the sum of all [`NameEntry`] structures when encoded +// /// into [`SftpSink`] format. The length is to be pre-computed by +// /// encoding each entry and summing the [`SftpSink::payload_len()`] values. +// fn get_encoded_len(&self) -> SftpOpResult { +// Err(StatusCode::SSH_FX_OP_UNSUPPORTED) +// } + +// /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter +// /// were a [`NameEntry`] has been encoded. +// /// +// /// +// #[allow(unused_variables)] +// fn for_each_encoded(&self, writer: F) -> SftpOpResult<()> +// where +// F: FnMut(&[u8]) -> (), +// { +// Err(StatusCode::SSH_FX_OP_UNSUPPORTED) +// } + +// // /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter +// // /// were a [`NameEntry`] has been encoded. +// // /// +// // /// +// // #[allow(unused_variables)] +// // fn encoded_iter( +// // &self, +// // writer: F, +// // ) -> impl Iterator> { +// // Err(StatusCode::SSH_FX_OP_UNSUPPORTED) +// // } +// } // TODO Define this /// **This is a work in progress** @@ -185,40 +185,38 @@ pub struct ChanOut<'g, 'a> { /// implementation via [`SftpServer::readdir()`] in order to send the /// directory content list. /// -/// It contains references to a slice buffer and a output channel +/// It handles immutable sending data via the underlying sftp-channel /// [`sunset_async::async_channel::ChanOut`] used in the context of an /// SFTP Session. /// /// The usage is simple: /// -/// 1. SftpHandler will: Initialize the structure with the buffer and the ChanOut +/// 1. SftpHandler will: Initialize the structure /// 2. The `SftpServer` trait implementation for `readdir()` will: -/// a. Receive the DirReply -/// b. Obtain the number of items and the **total** encoded length of -/// the [`NameEntry`] items in the directory and send them calling `send_header` -/// c. Send each serialized `NameEntry`, excluding their length using -/// the `send_item` method -/// +/// a. Receive the DirReply ref `reply` +/// b. Instantiate a [`DirEntriesCollection`] with the items in the requested folder +/// c. call the `DirEntriesCollection.SendHeader(reply)` +/// d. call the `DirEntriesCollection.send_entries(reply)` pub struct DirReply<'g, const N: usize> { - /// Used during the + /// The request Id that will be used in the response req_id: ReqId, - // _phantom_g: PhantomData<&'g ()>, - // /// To test muting operations + /// Immutable writer chan_out: &'g SftpOutputProducer<'g, N>, } impl<'g, const N: usize> DirReply<'g, N> { + /// New instance pub fn new(req_id: ReqId, chan_out: &'g SftpOutputProducer<'g, N>) -> Self { // DirReply { chan_out: chan_out_wrapper, req_id } DirReply { req_id, chan_out } } /// Sends an item to the client - pub async fn send_item(&self, data: &[u8]) { - // *self.muting += 1; - debug!("Sending item: len = {:?}, content = {:?}", data.len(), data); - self.chan_out.send_data(data).await; + pub async fn send_item(&self, data: &[u8]) -> SftpResult<()> { + debug!("Sending item: {:?} bytes", data.len()); + trace!("Sending item: content = {:?}", data); + self.chan_out.send_data(data).await } /// Sends the header to the client. TODO Make this enforceable From 188f748ddf7539cb6fb412f5851945e143cc640e Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 7 Nov 2025 12:12:02 +1100 Subject: [PATCH 147/393] [skip ci] WIP: Hack readdir response was not adding header field length packet type + request id + items (1 + 4 + 4 bytes) hardcoded but will refactor --- demo/sftp/std/src/demosftpserver.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 3d412cc7..dff9f82f 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -334,9 +334,9 @@ pub struct DirEntriesCollection { impl DirEntriesCollection { pub fn new(dir_iterator: fs::ReadDir) -> Self { - let mut encoded_length = 0; - // This way I collect data required for the header and collect - // valid entries into a vector (only std) + let mut encoded_length = 9; // TODO We need to consider the packet type, Id and count fields + // This way I collect data required for the header and collect + // valid entries into a vector (only std) let entries: Vec = dir_iterator .filter_map(|entry_result| { let entry = entry_result.ok()?; From 33104c6c9fbb7fe770084708af31a1be7f7738c9 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 7 Nov 2025 12:31:14 +1100 Subject: [PATCH 148/393] [skip ci] WIP: BUG in sftphandler process keeps responding to the same read dir request [skip ci] WIP: BUG **Sending loop includes sftp client** - the ReqId increases - The loop stop in a client interruption [ctl]+C I am going to need more visibility on why the client keeps on sending new request for the same files [skip ci] WIP: BUG I was not following V3 specification as did not send EOF https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7 Next issue is that I am sending the relative path and not just the filename. Will fix next --- demo/sftp/std/src/demosftpserver.rs | 5 + demo/sftp/std/src/main.rs | 37 ++++--- sftp/src/sftphandler/sftphandler.rs | 101 ++++++++---------- .../sftphandler/sftpoutputchannelhandler.rs | 2 +- sftp/src/sftpserver.rs | 22 ++-- 5 files changed, 84 insertions(+), 83 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index dff9f82f..091a0cfe 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -429,6 +429,11 @@ impl DirEntriesCollection { error!("SftpError: {:?}", err); StatusCode::SSH_FX_FAILURE })?; + + reply.send_eof().await.map_err(|err| { + error!("SftpError: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; } Ok(()) } diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 22baf1ec..84348a6c 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -205,28 +205,27 @@ async fn listen( #[embassy_executor::main] async fn main(spawner: Spawner) { env_logger::builder() - .filter_level(log::LevelFilter::Info) - .filter_module("sunset::runner", log::LevelFilter::Info) - // .filter_module("sunset::runner", log::LevelFilter::Trace) - // .filter_module("sunset::channel", log::LevelFilter::Trace) - .filter_module( - "sunset_sftp::sftphandler::sftpoutputchannelhandler", - log::LevelFilter::Debug, - ) - .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) - .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) + .filter_level(log::LevelFilter::Warn) .filter_module( "sunset_demo_sftp_std::demosftpserver", - log::LevelFilter::Debug, + log::LevelFilter::Info, ) - .filter_module("sunset_sftp::sftpserver", log::LevelFilter::Debug) - .filter_module("sunset::encrypt", log::LevelFilter::Info) - .filter_module("sunset::conn", log::LevelFilter::Info) - .filter_module("sunset::kex", log::LevelFilter::Trace) - .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) - .filter_module("async_io", log::LevelFilter::Info) - .filter_module("polling", log::LevelFilter::Info) - .filter_module("embassy_net", log::LevelFilter::Info) + .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Debug) + // .filter_module( + // "sunset_sftp::sftphandler::sftpoutputchannelhandler", + // log::LevelFilter::Trace, + // ) + // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) + // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) + // .filter_module("sunset_sftp::sftpserver", log::LevelFilter::Info) + // .filter_module("sunset::runner", log::LevelFilter::Info) + // .filter_module("sunset::encrypt", log::LevelFilter::Info) + // .filter_module("sunset::conn", log::LevelFilter::Info) + // .filter_module("sunset::kex", log::LevelFilter::Info) + // .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) + // .filter_module("async_io", log::LevelFilter::Info) + // .filter_module("polling", log::LevelFilter::Info) + // .filter_module("embassy_net", log::LevelFilter::Info) .format_timestamp_nanos() .init(); diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index d370cddc..e1d8a26b 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -151,25 +151,22 @@ where buffer_in: &[u8], output_producer: &SftpOutputProducer<'_, BUFFER_OUT_SIZE>, ) -> SftpResult<()> { - let in_len = buffer_in.len(); - let mut buffer_in_lower_index_bracket = 0; + let mut buf = buffer_in; - trace!("Received {:} bytes to process", in_len); + trace!("Received {:} bytes to process", buf.len()); if !matches!(self.state, SftpHandleState::Fragmented(_)) - & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) + & buf.len().lt(&SFTP_MINIMUM_PACKET_LEN) { return Err(WireError::PacketWrong.into()); } - while buffer_in_lower_index_bracket < in_len { + trace!("Entering loop to process the full received buffer"); + while buf.len() > 0 { debug!( - "Buffer In Lower index bracket: {}", - buffer_in_lower_index_bracket - ); - debug!( - "<=======================[ SFTP Process State: {:?} ]=======================>", - self.state + "<=======================[ SFTP Process State: {:?} ]=======================> Buffer remaining: {}", + self.state, + buf.len() ); match &self.state { @@ -178,9 +175,7 @@ where FragmentedRequestState::ProcessingClippedRequest => { if let Err(e) = self .incomplete_request_holder - .try_append_for_valid_request( - &buffer_in[buffer_in_lower_index_bracket..], - ) + .try_append_for_valid_request(&buf) { match e { RequestHolderError::RanOut => { @@ -188,8 +183,9 @@ where "There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); - buffer_in_lower_index_bracket += - self.incomplete_request_holder.appended(); + buf = &buf[self + .incomplete_request_holder + .appended()..]; continue; } RequestHolderError::WireError(WireError::RanOut) => { @@ -197,8 +193,9 @@ where "WIRE ERROR: There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); - buffer_in_lower_index_bracket += - self.incomplete_request_holder.appended(); + buf = &buf[self + .incomplete_request_holder + .appended()..]; continue; } RequestHolderError::NoRoom => { @@ -223,7 +220,7 @@ where } let used = self.incomplete_request_holder.appended(); - buffer_in_lower_index_bracket += used; + buf = &buf[used..]; let mut source = SftpSource::new( &self.incomplete_request_holder.try_get_ref()?, @@ -279,9 +276,7 @@ where } } FragmentedRequestState::ProcessingLongRequest => { - let mut source = SftpSource::new( - &buffer_in[buffer_in_lower_index_bracket..], - ); + let mut source = SftpSource::new(&buf); trace!("Source content: {:?}", source); let mut write_tracker = if let Some(wt) = @@ -356,15 +351,12 @@ where self.state = SftpHandleState::Idle; } }; - buffer_in_lower_index_bracket = in_len - source.remaining(); + buf = &buf[buf.len() - source.remaining()..]; } }, SftpHandleState::Initializing => { - let (source, sftp_packet) = create_sftp_source_and_packet( - buffer_in, - buffer_in_lower_index_bracket, - ); + let (source, sftp_packet) = create_sftp_source_and_packet(buf); match sftp_packet { Ok(request) => { match request { @@ -394,13 +386,11 @@ where return Err(SftpError::MalformedPacket); } } - buffer_in_lower_index_bracket = in_len - source.remaining(); + buf = &buf[buf.len() - source.remaining()..]; } SftpHandleState::Idle => { - let (mut source, sftp_packet) = create_sftp_source_and_packet( - buffer_in, - buffer_in_lower_index_bracket, - ); + let (mut source, sftp_packet) = + create_sftp_source_and_packet(buf); match sftp_packet { Ok(request) => { Self::handle_general_request( @@ -427,8 +417,7 @@ where Ok(holder) => { self.partial_write_request_tracker = Some(holder); - self.state = - SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); } Err(e) => { error!("Error handle_ran_out"); @@ -436,14 +425,11 @@ where SftpError::WireError( WireError::RanOut, ) => { - debug!(""); - let read = self.incomplete_request_holder - .try_hold( - &buffer_in - [buffer_in_lower_index_bracket..], - )?; - buffer_in_lower_index_bracket += - read; + let read = self + .incomplete_request_holder + .try_hold(&buf)?; + buf = &buf[read..]; + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); continue; } @@ -466,11 +452,13 @@ where } }, }; - buffer_in_lower_index_bracket = in_len - source.remaining(); + buf = &buf[buf.len() - source.remaining()..]; + trace!("New buffer len {} bytes ", buf.len()) } } + trace!("Process checking buf len {:?}", buf.len()); } - + trace!("Exiting process with Ok(())"); Ok(()) } @@ -494,6 +482,8 @@ where let processing_loop = async { loop { let lr = chan_in.read(buffer_in).await?; + + debug!("SFTP <---- received: {:?} bytes", lr); trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); if lr == 0 { debug!("client disconnected"); @@ -627,13 +617,18 @@ where // TODO Implement the mechanism you are going to use to // handle the list of elements - let mut dir_reply = DirReply::new(req_id, output_producer); + let dir_reply = DirReply::new(req_id, output_producer); match file_server - .readdir(&T::try_from(&read_dir.handle)?, &mut dir_reply) + .readdir(&T::try_from(&read_dir.handle)?, &dir_reply) .await { - Ok(_) => {} + Ok(_) => { + // dir_reply should have sent a response + // output_producer + // .send_status(req_id, StatusCode::SSH_FX_EOF, "") + // .await?; + } Err(status) => { error!("Open failed: {:?}", status); @@ -732,18 +727,14 @@ where // Ok(()) } } + /// Function to create an SFTP source and decode an SFTP packet from it /// to avoid code duplication fn create_sftp_source_and_packet( - buffer_in: &[u8], - buffer_in_lower_index_bracket: usize, + buf: &[u8], ) -> (SftpSource<'_>, Result, WireError>) { - debug!( - "Creating a source: lower_index_bracket = {:?}, buffer_len = {:?}", - buffer_in_lower_index_bracket, - buffer_in.len() - ); - let mut source = SftpSource::new(&buffer_in[buffer_in_lower_index_bracket..]); + debug!("Creating a source: buf_len = {:?}", buf.len()); + let mut source = SftpSource::new(&buf); let sftp_packet = SftpPacket::decode_request(&mut source); (source, sftp_packet) diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 085e86f8..ad2e8a30 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -131,7 +131,7 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { req_id, Status { code: status, message: msg.into(), lang: "en-US".into() }, ); - debug!("Output Producer: Pushing a status message: {:?}", response); + trace!("Output Producer: Pushing a status message: {:?}", response); self.send_packet(&response).await?; Ok(()) } diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 21e2ea64..2482024b 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -197,6 +197,7 @@ pub struct ChanOut<'g, 'a> { /// b. Instantiate a [`DirEntriesCollection`] with the items in the requested folder /// c. call the `DirEntriesCollection.SendHeader(reply)` /// d. call the `DirEntriesCollection.send_entries(reply)` +/// e. call the `reply.send_eof()` pub struct DirReply<'g, const N: usize> { /// The request Id that will be used in the response req_id: ReqId, @@ -212,13 +213,6 @@ impl<'g, const N: usize> DirReply<'g, N> { DirReply { req_id, chan_out } } - /// Sends an item to the client - pub async fn send_item(&self, data: &[u8]) -> SftpResult<()> { - debug!("Sending item: {:?} bytes", data.len()); - trace!("Sending item: content = {:?}", data); - self.chan_out.send_data(data).await - } - /// Sends the header to the client. TODO Make this enforceable pub async fn send_header( &self, @@ -233,7 +227,7 @@ impl<'g, const N: usize> DirReply<'g, N> { let mut sink = SftpSink::new(&mut s); get_encoded_len.enc(&mut sink)?; - 104u8.enc(&mut sink)?; + 104u8.enc(&mut sink)?; // TODO Remove hack self.req_id.enc(&mut sink)?; get_count.enc(&mut sink)?; let payload = sink.payload_slice(); @@ -245,4 +239,16 @@ impl<'g, const N: usize> DirReply<'g, N> { self.chan_out.send_data(sink.payload_slice()).await?; Ok(()) } + + /// Sends an item to the client + pub async fn send_item(&self, data: &[u8]) -> SftpResult<()> { + debug!("Sending item: {:?} bytes", data.len()); + trace!("Sending item: content = {:?}", data); + self.chan_out.send_data(data).await + } + + /// Sends EOF meaning that there is no more files in the directory + pub async fn send_eof(&self) -> SftpResult<()> { + self.chan_out.send_status(self.req_id, StatusCode::SSH_FX_EOF, "").await + } } From 1712866d6df0e0798fb998bf93657880e57d0135 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 7 Nov 2025 19:05:43 +1100 Subject: [PATCH 149/393] [skip ci] WIP: BUG The EOF needs to be sent once the readdir has sent all the items and is asked again. Anything else fails This is point I have not considered. I could have sent one item at a time, with different challenges. In my opinion I should add this behavior to the SftpServer implementation, but that passes the responsibility to the library user and the idea is making the user not needing to care too much about the protocol implementation [skip ci] WIP: Typo and cleanup commented out code --- demo/sftp/std/src/demosftpserver.rs | 26 ++++++---- sftp/src/sftphandler/sftphandler.rs | 14 ++++-- sftp/src/sftpserver.rs | 74 +++++------------------------ 3 files changed, 38 insertions(+), 76 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 091a0cfe..e0a93183 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -298,9 +298,11 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { let name_entry_collection = DirEntriesCollection::new(dir_iterator); - name_entry_collection.send_header(reply).await?; + name_entry_collection.send_entries_header(reply).await?; name_entry_collection.send_entries(reply).await?; + + // name_entry_collection.send_eof(reply).await?; } else { error!("the path is not a directory = {:?}", dir_path); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); @@ -340,8 +342,7 @@ impl DirEntriesCollection { let entries: Vec = dir_iterator .filter_map(|entry_result| { let entry = entry_result.ok()?; - - let filename = entry.path().to_string_lossy().into_owned(); + let filename = entry.file_name().to_string_lossy().into_owned(); let name_entry = NameEntry { filename: Filename::from(filename.as_str()), _longname: Filename::from(""), @@ -396,7 +397,7 @@ impl DirEntriesCollection { } } - pub async fn send_header( + pub async fn send_entries_header( &self, reply: &DirReply<'_, N>, ) -> SftpOpResult<()> { @@ -411,7 +412,7 @@ impl DirEntriesCollection { reply: &DirReply<'_, N>, ) -> SftpOpResult<()> { for entry in &self.entries { - let filename = entry.path().to_string_lossy().into_owned(); + let filename = entry.file_name().to_string_lossy().into_owned(); let attrs = Self::get_attrs_or_empty(entry.metadata()); let name_entry = NameEntry { filename: Filename::from(filename.as_str()), @@ -429,12 +430,17 @@ impl DirEntriesCollection { error!("SftpError: {:?}", err); StatusCode::SSH_FX_FAILURE })?; - - reply.send_eof().await.map_err(|err| { - error!("SftpError: {:?}", err); - StatusCode::SSH_FX_FAILURE - })?; } Ok(()) } + + pub async fn no_files( + &self, + reply: &DirReply<'_, N>, + ) -> SftpOpResult<()> { + reply.send_eof().await.map_err(|err| { + error!("SftpError: {:?}", err); + StatusCode::SSH_FX_FAILURE + }) + } } diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index e1d8a26b..f55d3285 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -614,9 +614,14 @@ where }; } SftpPacket::ReadDir(req_id, read_dir) => { - // TODO Implement the mechanism you are going to use to - // handle the list of elements - + // TODO I should send back an EOF response when all the files in folder have been sent AND I have been asked for more files. + // According to https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7 + // This should be the file_server responsibility + output_producer + .send_status(req_id, StatusCode::SSH_FX_EOF, "") + .await?; + + return Ok(()); let dir_reply = DirReply::new(req_id, output_producer); match file_server @@ -624,6 +629,7 @@ where .await { Ok(_) => { + // dir_reply should have sent a response // output_producer // .send_status(req_id, StatusCode::SSH_FX_EOF, "") @@ -646,7 +652,7 @@ where Ok(()) } - // TODO Handle more long requests + // TODO Handle other long requests /// Some long request will not fit in the channel buffers. Such requests /// will require to be handled differently. Gathering the data in and /// processing it as we receive it in the channel in buffer. diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 2482024b..227444a8 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -89,15 +89,6 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - // async fn readdir( - // &mut self, - // opaque_dir_handle: &T, - // reply: &DirReply<'_, N>, - // ) -> SftpOpResult<()> { - // log::error!("SftpServer ReadDir operation not defined"); - // Err(StatusCode::SSH_FX_OP_UNSUPPORTED) - // } - /// Provides the real path of the directory specified fn realpath(&mut self, dir: &str) -> SftpOpResult> { log::error!("SftpServer RealPath operation not defined: dir = {:?}", dir); @@ -105,54 +96,6 @@ where } } -// /// This trait is an standardized way to interact with an iterator or collection of Directory entries -// /// that need to be sent via an SSH_FXP_READDIR SFTP response to a client. -// /// -// /// It uses is expected when implementing an [`SftpServer`] TODO Future trait WIP -// pub trait DirEntriesResponseHelpers { -// /// returns the number of directory entries. -// /// Used for the `SSH_FXP_READDIR` response field `count` -// /// as specified in [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) -// fn get_count(&self) -> SftpOpResult { -// Err(StatusCode::SSH_FX_OP_UNSUPPORTED) -// } - -// /// Returns the total encoded length in bytes for all directory entries. -// /// Used for the `SSH_FXP_READDIR` general response field `length` -// /// as part of the [General Packet Format](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-3) -// /// -// /// This represents the sum of all [`NameEntry`] structures when encoded -// /// into [`SftpSink`] format. The length is to be pre-computed by -// /// encoding each entry and summing the [`SftpSink::payload_len()`] values. -// fn get_encoded_len(&self) -> SftpOpResult { -// Err(StatusCode::SSH_FX_OP_UNSUPPORTED) -// } - -// /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter -// /// were a [`NameEntry`] has been encoded. -// /// -// /// -// #[allow(unused_variables)] -// fn for_each_encoded(&self, writer: F) -> SftpOpResult<()> -// where -// F: FnMut(&[u8]) -> (), -// { -// Err(StatusCode::SSH_FX_OP_UNSUPPORTED) -// } - -// // /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter -// // /// were a [`NameEntry`] has been encoded. -// // /// -// // /// -// // #[allow(unused_variables)] -// // fn encoded_iter( -// // &self, -// // writer: F, -// // ) -> impl Iterator> { -// // Err(StatusCode::SSH_FX_OP_UNSUPPORTED) -// // } -// } - // TODO Define this /// **This is a work in progress** /// A reference structure passed to the [`SftpServer::read()`] method to @@ -193,11 +136,18 @@ pub struct ChanOut<'g, 'a> { /// /// 1. SftpHandler will: Initialize the structure /// 2. The `SftpServer` trait implementation for `readdir()` will: -/// a. Receive the DirReply ref `reply` -/// b. Instantiate a [`DirEntriesCollection`] with the items in the requested folder -/// c. call the `DirEntriesCollection.SendHeader(reply)` -/// d. call the `DirEntriesCollection.send_entries(reply)` -/// e. call the `reply.send_eof()` +/// +/// - Receive the DirReply ref `reply` +/// +/// a. If there are items to send: +/// +/// - Instantiate a [`DirEntriesCollection`] with the items in the requested folder +/// - call the `DirEntriesCollection.SendHeader(reply)` +/// - call the `DirEntriesCollection.send_entries(reply)` +/// +/// b. If there are no items to send: +/// +/// - Call the `reply.send_eof()` pub struct DirReply<'g, const N: usize> { /// The request Id that will be used in the response req_id: ReqId, From 37ac5079917f04adda0af27d739a7116878cd948 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 7 Nov 2025 19:56:48 +1100 Subject: [PATCH 150/393] [skip ci] WIP: Added a simplistic last read status approach Found a bug sending back the status code. It is always Ok! [skip ci] WIP: Temporary ways to track completed Read operations I will add a test to understand the unexpected behaviour in response status packet --- demo/sftp/std/src/demosftpserver.rs | 8 +++-- demo/sftp/std/src/main.rs | 8 ++--- sftp/src/lib.rs | 1 + sftp/src/sftphandler/sftphandler.rs | 52 ++++++++++++++++++++++------- sftp/src/sftpserver.rs | 19 ++++++++++- 5 files changed, 68 insertions(+), 20 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index e0a93183..f7fde1cc 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -7,7 +7,9 @@ use sunset::sshwire::SSHEncode; use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; use sunset_sftp::protocol::constants::MAX_NAME_ENTRY_SIZE; use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; -use sunset_sftp::server::{DirReply, ReadReply, SftpOpResult, SftpServer, SftpSink}; +use sunset_sftp::server::{ + DirReply, ReadReply, ReadStatus, SftpOpResult, SftpServer, SftpSink, +}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; @@ -275,7 +277,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { &mut self, opaque_dir_handle: &DemoOpaqueFileHandle, reply: &DirReply<'_, N>, - ) -> SftpOpResult<()> { + ) -> SftpOpResult { debug!("read dir for {:?}", opaque_dir_handle); if let PrivatePathHandle::Directory(dir) = self @@ -303,6 +305,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { name_entry_collection.send_entries(reply).await?; // name_entry_collection.send_eof(reply).await?; + Ok(ReadStatus::EndOfFile) } else { error!("the path is not a directory = {:?}", dir_path); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); @@ -311,7 +314,6 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { error!("Could not find the directory for {:?}", opaque_dir_handle); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); } - Ok(()) } } diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 84348a6c..ff7cbd4c 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -211,10 +211,10 @@ async fn main(spawner: Spawner) { log::LevelFilter::Info, ) .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Debug) - // .filter_module( - // "sunset_sftp::sftphandler::sftpoutputchannelhandler", - // log::LevelFilter::Trace, - // ) + .filter_module( + "sunset_sftp::sftphandler::sftpoutputchannelhandler", + log::LevelFilter::Trace, + ) // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) // .filter_module("sunset_sftp::sftpserver", log::LevelFilter::Info) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index accfcab2..751b0fa9 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -72,6 +72,7 @@ pub mod server { // pub use crate::sftpserver::DirEntriesResponseHelpers; pub use crate::sftpserver::DirReply; pub use crate::sftpserver::ReadReply; + pub use crate::sftpserver::ReadStatus; pub use crate::sftpserver::SftpOpResult; pub use crate::sftpserver::SftpServer; pub use crate::sftpsink::SftpSink; diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index f55d3285..230355a7 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -4,10 +4,10 @@ use crate::error::SftpError; use crate::handles::OpaqueFileHandle; use crate::proto::{ self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, - SftpPacket, StatusCode, + SftpPacket, Status, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; -use crate::server::{DirReply, SftpOpResult}; +use crate::server::{DirReply, ReadStatus, SftpOpResult, SftpSink}; use crate::sftperror::SftpResult; use crate::sftphandler::sftpoutputchannelhandler::{ SftpOutputPipe, SftpOutputProducer, @@ -17,7 +17,7 @@ use crate::sftpsource::SftpSource; use embassy_futures::select::select; use sunset::Error as SunsetError; -use sunset::sshwire::{SSHSource, WireError}; +use sunset::sshwire::{SSHEncode, SSHSource, WireError}; use sunset_async::ChanInOut; use core::u32; @@ -108,6 +108,12 @@ where partial_write_request_tracker: Option>, incomplete_request_holder: RequestHolder<'a>, + + /// Records the last read status reported + /// from a read operation. This is used to communicate + /// the EOF with the appropriate ReqId to a pending read operation + /// **WARNING**: This assumes that only one read operation is pending at a time. + last_read_status: ReadStatus, } impl<'a, T, S, const BUFFER_OUT_SIZE: usize> SftpHandler<'a, T, S, BUFFER_OUT_SIZE> @@ -132,6 +138,7 @@ where partial_write_request_tracker: None, state: SftpHandleState::default(), incomplete_request_holder: RequestHolder::new(incomplete_request_buffer), + last_read_status: ReadStatus::PendingData, } } @@ -231,6 +238,7 @@ where Ok(request) => { Self::handle_general_request( &mut self.file_server, + &mut self.last_read_status, output_producer, request, ) @@ -395,6 +403,7 @@ where Ok(request) => { Self::handle_general_request( &mut self.file_server, + &mut self.last_read_status, output_producer, request, ) @@ -508,6 +517,7 @@ where async fn handle_general_request( file_server: &mut S, + last_read_status: &mut ReadStatus, output_producer: &SftpOutputProducer<'_, BUFFER_OUT_SIZE>, request: SftpPacket<'_>, ) -> Result<(), SftpError> @@ -617,23 +627,41 @@ where // TODO I should send back an EOF response when all the files in folder have been sent AND I have been asked for more files. // According to https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7 // This should be the file_server responsibility - output_producer - .send_status(req_id, StatusCode::SSH_FX_EOF, "") - .await?; - return Ok(()); + if (*last_read_status).eq(&ReadStatus::EndOfFile) { + let packet = SftpPacket::Status( + req_id, + Status { + code: StatusCode::SSH_FX_EOF, + message: "".into(), + lang: "en-US".into(), + }, + ); + let mut buf = [0u8; 256]; + let mut sink = SftpSink::new(&mut buf); + packet.encode_response(&mut sink)?; + debug!("Output Producer: Sending packet {:?}", packet); + sink.finalize(); + output_producer.send_data(sink.used_slice()).await?; + // output_producer + // .send_status(req_id, StatusCode::SSH_FX_EOF, "") + // .await?; + *last_read_status = ReadStatus::PendingData; + return Ok(()); + } + let dir_reply = DirReply::new(req_id, output_producer); match file_server .readdir(&T::try_from(&read_dir.handle)?, &dir_reply) .await { - Ok(_) => { - + Ok(read_status) => { + *last_read_status = read_status; // dir_reply should have sent a response - // output_producer - // .send_status(req_id, StatusCode::SSH_FX_EOF, "") - // .await?; + output_producer + .send_status(req_id, StatusCode::SSH_FX_EOF, "") + .await?; } Err(status) => { error!("Open failed: {:?}", status); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 227444a8..a2038a6d 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -15,6 +15,22 @@ use sunset::sshwire::SSHEncode; /// Result used to store the result of an Sftp Operation pub type SftpOpResult = core::result::Result; +/// Since the server needs to answer with an STATUS EOF to finish read requests, +/// Helps handling the completion for reading data. +/// See: +/// +/// - [Reading and Writing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) +/// - [Scanning Directories](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7) +#[derive(PartialEq, Debug, Default)] +pub enum ReadStatus { + // TODO Ideally this will contain an OwnedFileHandle + /// There is more data to read + #[default] + PendingData, + /// The server has provided all the data requested + EndOfFile, +} + /// All trait functions are optional in the SFTP protocol. /// Some less core operations have a Provided implementation returning /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, @@ -81,7 +97,7 @@ where &mut self, opaque_dir_handle: &T, reply: &DirReply<'_, N>, - ) -> SftpOpResult<()> { + ) -> SftpOpResult { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", opaque_dir_handle @@ -132,6 +148,7 @@ pub struct ChanOut<'g, 'a> { /// [`sunset_async::async_channel::ChanOut`] used in the context of an /// SFTP Session. /// +// TODO: complete this once the flow is fully developed /// The usage is simple: /// /// 1. SftpHandler will: Initialize the structure From 2352fa935122fc2dfa5f57334417675fbd9e821d Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 10 Nov 2025 10:53:12 +1100 Subject: [PATCH 151/393] [skip ci] WIP: Missing StatusCode field in Status derived from SSHEncode implementation for enum sshwire-derive lib.rs defines the rules to encode enums in `encode_enum` and the encoding decision is not to encode the value. See [implementation here](https://github.com/mkj/sunset/blob/8e5d20916cf7b29111b90e4d3b7bb7827c9be8e5/sshwire-derive/src/lib.rs#L287) This could be addressed by introducing a parameter for enum fields where the encoded data type is declared (or not). There is probably a good reason for this behavior. --- sftp/src/proto.rs | 37 +++++++++++++++++++++++++++++++++++++ sftp/src/sftpserver.rs | 36 ++++++++++++++++++------------------ sftp/src/sftpsink.rs | 1 + 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index a1bddeed..7fa4a62e 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -250,6 +250,7 @@ pub struct ResponseAttributes { pub struct ReqId(pub u32); /// For more information see [Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) +/// TODO: Reference! This is packed as u32 since that is the field data type in specs #[derive(Debug, FromPrimitive, SSHEncode)] #[repr(u32)] #[allow(non_camel_case_types, missing_docs)] @@ -794,3 +795,39 @@ sftpmessages! [ (104, Name, Name<'a>, "ssh_fxp_name"), }, ]; + +#[cfg(test)] +mod ProtoTests { + use super::*; + use crate::server::SftpSink; + + #[test] + fn test_status_encoding() { + let mut buf = [0u8; 256]; + let mut sink = SftpSink::new(&mut buf); + let status_packet = SftpPacket::Status( + ReqId(16), + Status { + code: StatusCode::SSH_FX_EOF, + message: "A".into(), + lang: "en-US".into(), + }, + ); + + let expected_status_packet_slice: [u8; 27] = [ + 0, 0, 0, 18, // Packet len + 101, // Packet type + 0, 0, 0, 16, // ReqId + 0, 0, 0, 4, // Status code + 0, 0, 0, 1, // string message length + 65, // string message content + 0, 0, 0, 5, // string lang length + 101, 110, 45, 85, 83, // string lang content + ]; + + let _ = status_packet.encode_response(&mut sink); + sink.finalize(); + + assert_eq!(&expected_status_packet_slice, sink.used_slice()); + } +} diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index a2038a6d..66e21924 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -138,6 +138,23 @@ pub struct ChanOut<'g, 'a> { _phantom_a: PhantomData<&'a ()>, // a' Why the second lifetime if ChanIO only needs one } +// TODO: complete this once the flow is fully developed +// `/ The usage is simple: +// / +// / 1. SftpHandler will: Initialize the structure +// / 2. The `SftpServer` trait implementation for 'readdir()' will: +// / +// / - Receive the DirReply ref 'reply' +// / +// / a. If there are items to send: +// / +// / - Instantiate a [`DirEntriesCollection`] with the items in the requested folder +// / - call the 'DirEntriesCollection.SendHeader(reply)' +// / - call the 'DirEntriesCollection.send_entries(reply)' +// / +// / b. If there are no items to send: +// / +// / - Call the 'reply.send_eof()' // TODO Define this /// Dir Reply is the structure that will be "visiting" the [`SftpServer`] /// trait @@ -148,25 +165,8 @@ pub struct ChanOut<'g, 'a> { /// [`sunset_async::async_channel::ChanOut`] used in the context of an /// SFTP Session. /// -// TODO: complete this once the flow is fully developed -/// The usage is simple: -/// -/// 1. SftpHandler will: Initialize the structure -/// 2. The `SftpServer` trait implementation for `readdir()` will: -/// -/// - Receive the DirReply ref `reply` -/// -/// a. If there are items to send: -/// -/// - Instantiate a [`DirEntriesCollection`] with the items in the requested folder -/// - call the `DirEntriesCollection.SendHeader(reply)` -/// - call the `DirEntriesCollection.send_entries(reply)` -/// -/// b. If there are no items to send: -/// -/// - Call the `reply.send_eof()` pub struct DirReply<'g, const N: usize> { - /// The request Id that will be used in the response + /// The request Id that will be use`d in the response req_id: ReqId, /// Immutable writer diff --git a/sftp/src/sftpsink.rs b/sftp/src/sftpsink.rs index 08fa7f9e..9fa221da 100644 --- a/sftp/src/sftpsink.rs +++ b/sftp/src/sftpsink.rs @@ -25,6 +25,7 @@ impl<'g> SftpSink<'g> { SftpSink { buffer: s, index: SFTP_FIELD_LEN_LENGTH } } + // TODO: Why don't you compute this every time that a new field is added? /// Finalise the buffer by prepending the packet length field, /// excluding the field itself. /// From c7927451757426b9a98f9ae3dbc1b05a43caaad8 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 12 Nov 2025 08:30:57 +1100 Subject: [PATCH 152/393] [skip ci] Fixed Status Encoding. Shall I extend the sshwire-derive to generate encoded values? To fix the issue with the message type encoding I have done a manual implementation of SSHEncode for `StatusCode`. It is tested in a proto_test mod. From last commit: sshwire-derive lib.rs defines the rules to encode enums in `encode_enum` and the encoding decision is not to encode the value. See [implementation here](https://github.com/mkj/sunset/blob/8e5d20916cf7b29111b90e4d3b7bb7827c9be8e5/sshwire-derive/src/lib.rs#L287) This could be addressed by introducing a parameter for enum fields where the encoded data type is declared (or not). There is probably a good reason for this behavior. --- sftp/src/proto.rs | 55 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 7fa4a62e..313f6ecd 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -166,6 +166,7 @@ pub struct Status<'a> { /// A language tag as defined by [Tags for the Identification of Languages](https://datatracker.ietf.org/doc/html/rfc1766) pub lang: TextString<'a>, } + /// Used for `ssh_fxp_handle` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7). #[derive(Debug, Clone, Copy, PartialEq, Eq, SSHEncode, SSHDecode)] pub struct Handle<'a> { @@ -251,29 +252,29 @@ pub struct ReqId(pub u32); /// For more information see [Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) /// TODO: Reference! This is packed as u32 since that is the field data type in specs -#[derive(Debug, FromPrimitive, SSHEncode)] +#[derive(Debug, FromPrimitive)] #[repr(u32)] #[allow(non_camel_case_types, missing_docs)] pub enum StatusCode { - #[sshwire(variant = "ssh_fx_ok")] + // #[sshwire(variant = "ssh_fx_ok")] SSH_FX_OK = 0, - #[sshwire(variant = "ssh_fx_eof")] + // #[sshwire(variant = "ssh_fx_eof")] SSH_FX_EOF = 1, - #[sshwire(variant = "ssh_fx_no_such_file")] + // #[sshwire(variant = "ssh_fx_no_such_file")] SSH_FX_NO_SUCH_FILE = 2, - #[sshwire(variant = "ssh_fx_permission_denied")] + // #[sshwire(variant = "ssh_fx_permission_denied")] SSH_FX_PERMISSION_DENIED = 3, - #[sshwire(variant = "ssh_fx_failure")] + // #[sshwire(variant = "ssh_fx_failure")] SSH_FX_FAILURE = 4, - #[sshwire(variant = "ssh_fx_bad_message")] + // #[sshwire(variant = "ssh_fx_bad_message")] SSH_FX_BAD_MESSAGE = 5, - #[sshwire(variant = "ssh_fx_no_connection")] + // #[sshwire(variant = "ssh_fx_no_connection")] SSH_FX_NO_CONNECTION = 6, - #[sshwire(variant = "ssh_fx_connection_lost")] + // #[sshwire(variant = "ssh_fx_connection_lost")] SSH_FX_CONNECTION_LOST = 7, - #[sshwire(variant = "ssh_fx_unsupported")] + // #[sshwire(variant = "ssh_fx_unsupported")] SSH_FX_OP_UNSUPPORTED = 8, - #[sshwire(unknown)] + // #[sshwire(unknown)] #[num_enum(catch_all)] Other(u32), } @@ -287,6 +288,32 @@ impl<'de> SSHDecode<'de> for StatusCode { } } +// TODO: Implement an automatic from implementation for u32 to Status code +// This is prone to errors if we update StatusCode enum +impl From<&StatusCode> for u32 { + fn from(value: &StatusCode) -> Self { + match value { + StatusCode::SSH_FX_OK => 0, + StatusCode::SSH_FX_EOF => 1, + StatusCode::SSH_FX_NO_SUCH_FILE => 2, + StatusCode::SSH_FX_PERMISSION_DENIED => 3, + StatusCode::SSH_FX_FAILURE => 4, + StatusCode::SSH_FX_BAD_MESSAGE => 5, + StatusCode::SSH_FX_NO_CONNECTION => 6, + StatusCode::SSH_FX_CONNECTION_LOST => 7, + StatusCode::SSH_FX_OP_UNSUPPORTED => 8, + StatusCode::Other(value) => *value, + } + } +} +// TODO: Implement an SSHEncode attribute for enums to encode them in a given numeric format +impl SSHEncode for StatusCode { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + let numeric_value: u32 = self.into(); + numeric_value.enc(s) + } +} + #[derive(Debug, SSHEncode, SSHDecode)] pub struct ExtPair<'a> { pub name: &'a str, @@ -797,7 +824,7 @@ sftpmessages! [ ]; #[cfg(test)] -mod ProtoTests { +mod proto_tests { use super::*; use crate::server::SftpSink; @@ -815,10 +842,10 @@ mod ProtoTests { ); let expected_status_packet_slice: [u8; 27] = [ - 0, 0, 0, 18, // Packet len + 0, 0, 0, 23, // Packet len 101, // Packet type 0, 0, 0, 16, // ReqId - 0, 0, 0, 4, // Status code + 0, 0, 0, 1, // Status code: SSH_FX_EOF 0, 0, 0, 1, // string message length 65, // string message content 0, 0, 0, 5, // string lang length From 88ec688566e53728f129ab9bab579cb4e079e3e8 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 12 Nov 2025 08:57:46 +1100 Subject: [PATCH 153/393] [skip ci] WIP: First ls command successful Not ready yet. In order to answer EOF to a SSH_SXP_READDIR after all the dir items have been already sent. I am storing a `sftpserver::ReadStatus` passed as a result on `SftpServer.dirread()` with PendingData or EndOfFile. This would not work well in scenarios where there is more than one read transaction in course. I have decided that I am going to make the SftpServer implementators the responsibility to send the EOF status message but I will help them by providing a detailed trait documentation. To test this listing you may run the demo/sftp/std and run the script testing/test_read_dir.sh mind that the hard link counter is not recovered as it is not part of SFTP V3 implementation --- demo/sftp/std/src/demosftpserver.rs | 1 - demo/sftp/std/testing/test_read_dir.sh | 4 ++-- sftp/src/sftphandler/sftphandler.rs | 12 ++++++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index f7fde1cc..567ce906 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -304,7 +304,6 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { name_entry_collection.send_entries(reply).await?; - // name_entry_collection.send_eof(reply).await?; Ok(ReadStatus::EndOfFile) } else { error!("the path is not a directory = {:?}", dir_path); diff --git a/demo/sftp/std/testing/test_read_dir.sh b/demo/sftp/std/testing/test_read_dir.sh index 75d0dcb3..3f8d163c 100755 --- a/demo/sftp/std/testing/test_read_dir.sh +++ b/demo/sftp/std/testing/test_read_dir.sh @@ -22,9 +22,9 @@ ls echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." # Upload all files -sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'put ./%s\n' "${FILES[@]}") -ls +ls -lh bye EOF diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 230355a7..2914f4c7 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -515,6 +515,14 @@ where } } + /// Handles Healthy formed SftpRequest. Will return error if: + /// + /// - The request (SftpPacket) is not a request + /// + /// - The request is an unknown SftpPacket + /// + /// - The request is an initialization packet, and the initialization + /// has already been performed async fn handle_general_request( file_server: &mut S, last_read_status: &mut ReadStatus, @@ -658,10 +666,6 @@ where { Ok(read_status) => { *last_read_status = read_status; - // dir_reply should have sent a response - output_producer - .send_status(req_id, StatusCode::SSH_FX_EOF, "") - .await?; } Err(status) => { error!("Open failed: {:?}", status); From 505fcf8fe1f9157db483e167c043ecdf6063c5dd Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 12 Nov 2025 10:42:49 +1100 Subject: [PATCH 154/393] [skip ci] Implementing EOF responsibility in SftpServer and documenting --- demo/sftp/std/src/demofilehandlemanager.rs | 4 + demo/sftp/std/src/demosftpserver.rs | 40 +++++----- demo/sftp/std/testing/test_read_dir.sh | 3 + sftp/src/opaquefilehandle.rs | 3 + sftp/src/sftphandler/sftphandler.rs | 59 +++----------- sftp/src/sftpserver.rs | 91 +++++++++++++++++----- 6 files changed, 111 insertions(+), 89 deletions(-) diff --git a/demo/sftp/std/src/demofilehandlemanager.rs b/demo/sftp/std/src/demofilehandlemanager.rs index feaa126f..6b1cb278 100644 --- a/demo/sftp/std/src/demofilehandlemanager.rs +++ b/demo/sftp/std/src/demofilehandlemanager.rs @@ -56,4 +56,8 @@ where fn get_private_as_ref(&self, opaque_handle: &K) -> Option<&V> { self.handle_map.get(opaque_handle) } + + fn get_private_as_mut_ref(&mut self, opaque_handle: &K) -> Option<&mut V> { + self.handle_map.get_mut(opaque_handle) + } } diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 567ce906..8952afbb 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -36,6 +36,7 @@ pub(crate) struct PrivateFileHandle { #[derive(Debug)] pub(crate) struct PrivateDirHandle { path: String, + read_status: ReadStatus, } static OPAQUE_SALT: &'static str = "12d%32"; @@ -152,7 +153,10 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { debug!("Open Directory = {:?}", dir); let dir_handle = self.handles_manager.insert( - PrivatePathHandle::Directory(PrivateDirHandle { path: dir.into() }), + PrivatePathHandle::Directory(PrivateDirHandle { + path: dir.into(), + read_status: ReadStatus::default(), + }), OPAQUE_SALT, ); @@ -277,14 +281,21 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { &mut self, opaque_dir_handle: &DemoOpaqueFileHandle, reply: &DirReply<'_, N>, - ) -> SftpOpResult { + ) -> SftpOpResult<()> { debug!("read dir for {:?}", opaque_dir_handle); if let PrivatePathHandle::Directory(dir) = self .handles_manager - .get_private_as_ref(opaque_dir_handle) + .get_private_as_mut_ref(opaque_dir_handle) .ok_or(StatusCode::SSH_FX_NO_SUCH_FILE)? { + if dir.read_status == ReadStatus::EndOfFile { + reply.send_eof().await.map_err(|error| { + error!("{:?}", error); + StatusCode::SSH_FX_FAILURE + })?; + return Ok(()); + } let path_str = dir.path.clone(); debug!("opaque handle found in handles manager: {:?}", path_str); let dir_path = Path::new(&path_str); @@ -303,8 +314,8 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { name_entry_collection.send_entries_header(reply).await?; name_entry_collection.send_entries(reply).await?; - - Ok(ReadStatus::EndOfFile) + dir.read_status = ReadStatus::EndOfFile; + return Ok(()); } else { error!("the path is not a directory = {:?}", dir_path); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); @@ -317,13 +328,11 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } // TODO Add this to SFTP library only available with std as a global helper -/// This is a helper structure to make ReadDir into something somehow -/// digestible by [`DirReply`] +/// This is a helper structure to make ReadDir into something manageable for +/// [`DirReply`] /// /// WIP: Not stable. It has know issues and most likely it's methods will change /// -/// BUG: It does not count properly the number of bytes -/// /// BUG: It does not include longname and that may be an issue #[derive(Debug)] pub struct DirEntriesCollection { @@ -337,9 +346,8 @@ pub struct DirEntriesCollection { impl DirEntriesCollection { pub fn new(dir_iterator: fs::ReadDir) -> Self { - let mut encoded_length = 9; // TODO We need to consider the packet type, Id and count fields - // This way I collect data required for the header and collect - // valid entries into a vector (only std) + let mut encoded_length = 0; + let entries: Vec = dir_iterator .filter_map(|entry_result| { let entry = entry_result.ok()?; @@ -421,13 +429,7 @@ impl DirEntriesCollection { attrs, }; debug!("Sending new item: {:?}", name_entry); - let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; - let mut sftp_sink = SftpSink::new(&mut buffer); - name_entry.enc(&mut sftp_sink).map_err(|err| { - error!("WireError: {:?}", err); - StatusCode::SSH_FX_FAILURE - })?; - reply.send_item(sftp_sink.payload_slice()).await.map_err(|err| { + reply.send_item(&name_entry).await.map_err(|err| { error!("SftpError: {:?}", err); StatusCode::SSH_FX_FAILURE })?; diff --git a/demo/sftp/std/testing/test_read_dir.sh b/demo/sftp/std/testing/test_read_dir.sh index 3f8d163c..29154da7 100755 --- a/demo/sftp/std/testing/test_read_dir.sh +++ b/demo/sftp/std/testing/test_read_dir.sh @@ -28,3 +28,6 @@ ls -lh bye EOF +echo "Cleaning up local files..." +rm -f -r ./*_random ./out/*_random + diff --git a/sftp/src/opaquefilehandle.rs b/sftp/src/opaquefilehandle.rs index 18f0662b..19450ef1 100644 --- a/sftp/src/opaquefilehandle.rs +++ b/sftp/src/opaquefilehandle.rs @@ -62,6 +62,9 @@ where /// Returns true if the opaque handle exist fn opaque_handle_exist(&self, opaque_handle: &K) -> bool; + /// given the opaque_handle returns a reference to the associated private handle + fn get_private_as_mut_ref(&mut self, opaque_handle: &K) -> Option<&mut V>; + /// given the opaque_handle returns a reference to the associated private handle fn get_private_as_ref(&self, opaque_handle: &K) -> Option<&V>; } diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 2914f4c7..ea70cdc9 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -107,13 +107,8 @@ where /// partially and the remaining is expected in successive buffers partial_write_request_tracker: Option>, + /// Used to handle received buffers that do not hold a complete request [`SftpPacket`] incomplete_request_holder: RequestHolder<'a>, - - /// Records the last read status reported - /// from a read operation. This is used to communicate - /// the EOF with the appropriate ReqId to a pending read operation - /// **WARNING**: This assumes that only one read operation is pending at a time. - last_read_status: ReadStatus, } impl<'a, T, S, const BUFFER_OUT_SIZE: usize> SftpHandler<'a, T, S, BUFFER_OUT_SIZE> @@ -138,7 +133,6 @@ where partial_write_request_tracker: None, state: SftpHandleState::default(), incomplete_request_holder: RequestHolder::new(incomplete_request_buffer), - last_read_status: ReadStatus::PendingData, } } @@ -238,7 +232,6 @@ where Ok(request) => { Self::handle_general_request( &mut self.file_server, - &mut self.last_read_status, output_producer, request, ) @@ -403,7 +396,6 @@ where Ok(request) => { Self::handle_general_request( &mut self.file_server, - &mut self.last_read_status, output_producer, request, ) @@ -525,7 +517,6 @@ where /// has already been performed async fn handle_general_request( file_server: &mut S, - last_read_status: &mut ReadStatus, output_producer: &SftpOutputProducer<'_, BUFFER_OUT_SIZE>, request: SftpPacket<'_>, ) -> Result<(), SftpError> @@ -632,48 +623,18 @@ where }; } SftpPacket::ReadDir(req_id, read_dir) => { - // TODO I should send back an EOF response when all the files in folder have been sent AND I have been asked for more files. - // According to https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7 - // This should be the file_server responsibility - - if (*last_read_status).eq(&ReadStatus::EndOfFile) { - let packet = SftpPacket::Status( - req_id, - Status { - code: StatusCode::SSH_FX_EOF, - message: "".into(), - lang: "en-US".into(), - }, - ); - let mut buf = [0u8; 256]; - let mut sink = SftpSink::new(&mut buf); - packet.encode_response(&mut sink)?; - debug!("Output Producer: Sending packet {:?}", packet); - sink.finalize(); - output_producer.send_data(sink.used_slice()).await?; - // output_producer - // .send_status(req_id, StatusCode::SSH_FX_EOF, "") - // .await?; - *last_read_status = ReadStatus::PendingData; - return Ok(()); - } - - let dir_reply = DirReply::new(req_id, output_producer); - - match file_server - .readdir(&T::try_from(&read_dir.handle)?, &dir_reply) + if let Err(status) = file_server + .readdir( + &T::try_from(&read_dir.handle)?, + &DirReply::new(req_id, output_producer), + ) .await { - Ok(read_status) => { - *last_read_status = read_status; - } - Err(status) => { - error!("Open failed: {:?}", status); + error!("Open failed: {:?}", status); - output_producer - .send_status(req_id, status, "Error Reading Directory") - .await?; - } + output_producer + .send_status(req_id, status, "Error Reading Directory") + .await?; }; } _ => { diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 66e21924..dbf489de 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,4 +1,5 @@ use crate::error::SftpResult; +use crate::proto::{MAX_NAME_ENTRY_SIZE, NameEntry}; use crate::server::SftpSink; use crate::sftphandler::SftpOutputProducer; use crate::{ @@ -7,7 +8,7 @@ use crate::{ }; use core::marker::PhantomData; -use log::{debug, trace}; +use log::{debug, error, trace}; use sunset::sshwire::SSHEncode; // use futures::executor::block_on; TODO Deal with the async nature of [`ChanOut`] @@ -15,8 +16,13 @@ use sunset::sshwire::SSHEncode; /// Result used to store the result of an Sftp Operation pub type SftpOpResult = core::result::Result; -/// Since the server needs to answer with an STATUS EOF to finish read requests, -/// Helps handling the completion for reading data. +/// To finish read requests the server needs to answer to +/// **subsequent READ requests** after all the data has been sent already +/// with a [`SftpPacket`] including a status code [`StatusCode::SSH_FX_EOF`]. +/// +/// [`ReadStatus`] enum has been implemented to keep record of these exhausted +/// read operations. +/// /// See: /// /// - [Reading and Writing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) @@ -24,10 +30,13 @@ pub type SftpOpResult = core::result::Result; #[derive(PartialEq, Debug, Default)] pub enum ReadStatus { // TODO Ideally this will contain an OwnedFileHandle - /// There is more data to read + /// There is more data to be read therefore the [`SftpServer`] will + /// send more data in the next read request. #[default] PendingData, - /// The server has provided all the data requested + /// The server has provided all the data requested therefore the [`SftpServer`] + /// will send a [`SftpPacket`] including a status code [`StatusCode::SSH_FX_EOF`] + /// in the next read request. EndOfFile, } @@ -91,13 +100,31 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - /// Reads the list of items in a directory + /// Reads the list of items in a directory and returns them using the [`DirReply`] + /// parameter. + /// + /// ## Notes to the implementer: + /// + /// The implementer is expected to use the parameter `reply` [`DirReply`] to: + /// + /// - In case of no more items in the directory to send, call `reply.send_eof()` + /// - There are more items in the directory: + /// 1. Call `reply.send_header()` with the number of items and the [`SSHEncode`] + /// length of all the items to be sent + /// 2. Call `reply.send_item()` for each of the items announced to be sent + /// 3. Do not call `reply.send_eof()` during this [`readdir`] method call + /// + /// The server is expected to keep track of the number of items that remain to be sent + /// to the client since the client will only stop asking for more elements in the + /// directory when a read dir request is answer with an reply.send_eof() + /// + #[allow(unused_variables)] async fn readdir( &mut self, opaque_dir_handle: &T, reply: &DirReply<'_, N>, - ) -> SftpOpResult { + ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", opaque_dir_handle @@ -156,10 +183,15 @@ pub struct ChanOut<'g, 'a> { // / // / - Call the 'reply.send_eof()' // TODO Define this -/// Dir Reply is the structure that will be "visiting" the [`SftpServer`] -/// trait -/// implementation via [`SftpServer::readdir()`] in order to send the -/// directory content list. + +/// Uses for [`DirReply`] to: +/// +/// - In case of no more items in the directory to be sent, call `reply.send_eof()` +/// - There are more items in the directory to be sent: +/// 1. Call `reply.send_header()` with the number of items and the [`SSHEncode`] +/// length of all the items to be sent +/// 2. Call `reply.send_item()` for each of the items announced to be sent +/// 3. Do not call `reply.send_eof()` during this [`readdir`] method call /// /// It handles immutable sending data via the underlying sftp-channel /// [`sunset_async::async_channel::ChanOut`] used in the context of an @@ -174,13 +206,19 @@ pub struct DirReply<'g, const N: usize> { } impl<'g, const N: usize> DirReply<'g, N> { - /// New instance - pub fn new(req_id: ReqId, chan_out: &'g SftpOutputProducer<'g, N>) -> Self { + /// New instances can only be created within the crate. Users can only + /// use other public methods to use it. + pub(crate) fn new( + req_id: ReqId, + chan_out: &'g SftpOutputProducer<'g, N>, + ) -> Self { // DirReply { chan_out: chan_out_wrapper, req_id } DirReply { req_id, chan_out } } - /// Sends the header to the client. TODO Make this enforceable + // TODO Make this enforceable + /// Sends the header to the client with the number of files as [`NameEntry`] and the [`SSHEncode`] + /// length of all these [`NameEntry`] items pub async fn send_header( &self, get_count: u32, @@ -194,9 +232,13 @@ impl<'g, const N: usize> DirReply<'g, N> { let mut sink = SftpSink::new(&mut s); get_encoded_len.enc(&mut sink)?; - 104u8.enc(&mut sink)?; // TODO Remove hack + 104u8.enc(&mut sink)?; // TODO Replace hack with self.req_id.enc(&mut sink)?; - get_count.enc(&mut sink)?; + let encoded_name_sftp_packet_length: u32 = 9; + // We need to consider the packet type, Id and count fields + // This way I collect data required for the header and collect + // valid entries into a vector (only std) + (get_count + encoded_name_sftp_packet_length).enc(&mut sink)?; let payload = sink.payload_slice(); debug!( "Sending header: len = {:?}, content = {:?}", @@ -207,11 +249,18 @@ impl<'g, const N: usize> DirReply<'g, N> { Ok(()) } - /// Sends an item to the client - pub async fn send_item(&self, data: &[u8]) -> SftpResult<()> { - debug!("Sending item: {:?} bytes", data.len()); - trace!("Sending item: content = {:?}", data); - self.chan_out.send_data(data).await + /// Sends a directory item to the client as a [`NameEntry`] + /// + /// Call this + pub async fn send_item(&self, name_entry: &NameEntry<'_>) -> SftpResult<()> { + let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; + let mut sftp_sink = SftpSink::new(&mut buffer); + name_entry.enc(&mut sftp_sink).map_err(|err| { + error!("WireError: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; + + self.chan_out.send_data(sftp_sink.payload_slice()).await } /// Sends EOF meaning that there is no more files in the directory From 09afb828bf4208c4fd45aae891b6e311f6358a55 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 12 Nov 2025 11:31:06 +1100 Subject: [PATCH 155/393] [skip ci] Added SunsetSftp Feature: Std Will be used for helpers only available to Std compilations --- demo/sftp/std/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/sftp/std/Cargo.toml b/demo/sftp/std/Cargo.toml index 1cce9708..8a3eaa5f 100644 --- a/demo/sftp/std/Cargo.toml +++ b/demo/sftp/std/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" sunset = { workspace = true, features = ["rsa", "std"] } sunset-async.workspace = true sunset-demo-common.workspace = true -sunset-sftp = { version = "0.1.0", path = "../../../sftp" } +sunset-sftp = { version = "0.1.0", path = "../../../sftp", features = ["std"] } # 131072 was determined empirically embassy-executor = { version = "0.7", features = [ From eeccc76bd59a38b0cf138460d5dca028708f9f20 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 12 Nov 2025 11:33:18 +1100 Subject: [PATCH 156/393] [skip ci] Extracted DirEntriesCollection and added to std helpers This way we keep from std users some of the protocol details --- demo/sftp/std/src/demosftpserver.rs | 139 ++------------------------ sftp/Cargo.toml | 6 ++ sftp/src/lib.rs | 5 +- sftp/src/sftpserver.rs | 150 +++++++++++++++++++++++++++- 4 files changed, 164 insertions(+), 136 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 8952afbb..27ef3420 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -3,21 +3,16 @@ use crate::{ demoopaquefilehandle::DemoOpaqueFileHandle, }; -use sunset::sshwire::SSHEncode; use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; -use sunset_sftp::protocol::constants::MAX_NAME_ENTRY_SIZE; use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; +use sunset_sftp::server::helpers::DirEntriesCollection; use sunset_sftp::server::{ - DirReply, ReadReply, ReadStatus, SftpOpResult, SftpServer, SftpSink, + DirReply, ReadReply, ReadStatus, SftpOpResult, SftpServer, }; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -use std::fs::DirEntry; -use std::os::linux::fs::MetadataExt; -use std::os::unix::fs::PermissionsExt; -use std::time::SystemTime; -use std::{fs, io}; +use std::fs; use std::{fs::File, os::unix::fs::FileExt, path::Path}; #[derive(Debug)] @@ -296,6 +291,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { })?; return Ok(()); } + let path_str = dir.path.clone(); debug!("opaque handle found in handles manager: {:?}", path_str); let dir_path = Path::new(&path_str); @@ -311,10 +307,10 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { let name_entry_collection = DirEntriesCollection::new(dir_iterator); - name_entry_collection.send_entries_header(reply).await?; + let response_read_status = + name_entry_collection.send_response(reply).await?; - name_entry_collection.send_entries(reply).await?; - dir.read_status = ReadStatus::EndOfFile; + dir.read_status = response_read_status; return Ok(()); } else { error!("the path is not a directory = {:?}", dir_path); @@ -326,124 +322,3 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } } } - -// TODO Add this to SFTP library only available with std as a global helper -/// This is a helper structure to make ReadDir into something manageable for -/// [`DirReply`] -/// -/// WIP: Not stable. It has know issues and most likely it's methods will change -/// -/// BUG: It does not include longname and that may be an issue -#[derive(Debug)] -pub struct DirEntriesCollection { - /// Number of elements - count: u32, - /// Computed length of all the encoded elements - encoded_length: u32, - /// The actual entries. As you can see these are DirEntry. This is a std choice - entries: Vec, -} - -impl DirEntriesCollection { - pub fn new(dir_iterator: fs::ReadDir) -> Self { - let mut encoded_length = 0; - - let entries: Vec = dir_iterator - .filter_map(|entry_result| { - let entry = entry_result.ok()?; - let filename = entry.file_name().to_string_lossy().into_owned(); - let name_entry = NameEntry { - filename: Filename::from(filename.as_str()), - _longname: Filename::from(""), - attrs: Self::get_attrs_or_empty(entry.metadata()), - }; - - let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; - let mut sftp_sink = SftpSink::new(&mut buffer); - name_entry.enc(&mut sftp_sink).ok()?; - //TODO remove this unchecked casting - encoded_length += sftp_sink.payload_len() as u32; - Some(entry) - }) - .collect(); - - //TODO remove this unchecked casting - let count = entries.len() as u32; - - info!( - "Processed {} entries, estimated serialized length: {}", - count, encoded_length - ); - - Self { count, encoded_length, entries } - } - - fn get_attrs_or_empty( - maybe_metadata: Result, - ) -> Attrs { - maybe_metadata.map(Self::get_attrs).unwrap_or_default() - } - - fn get_attrs(metadata: fs::Metadata) -> Attrs { - let time_to_u32 = |time_result: io::Result| { - time_result - .ok()? - .duration_since(SystemTime::UNIX_EPOCH) - .ok()? - .as_secs() - .try_into() - .ok() - }; - - Attrs { - size: Some(metadata.len()), - uid: Some(metadata.st_uid()), - gid: Some(metadata.st_gid()), - permissions: Some(metadata.permissions().mode()), - atime: time_to_u32(metadata.accessed()), - mtime: time_to_u32(metadata.modified()), - ext_count: None, - } - } - - pub async fn send_entries_header( - &self, - reply: &DirReply<'_, N>, - ) -> SftpOpResult<()> { - reply.send_header(self.count, self.encoded_length).await.map_err(|e| { - debug!("Could not send header {e:?}"); - StatusCode::SSH_FX_FAILURE - }) - } - - pub async fn send_entries( - &self, - reply: &DirReply<'_, N>, - ) -> SftpOpResult<()> { - for entry in &self.entries { - let filename = entry.file_name().to_string_lossy().into_owned(); - let attrs = Self::get_attrs_or_empty(entry.metadata()); - let name_entry = NameEntry { - filename: Filename::from(filename.as_str()), - _longname: Filename::from(""), - attrs, - }; - debug!("Sending new item: {:?}", name_entry); - reply.send_item(&name_entry).await.map_err(|err| { - error!("SftpError: {:?}", err); - StatusCode::SSH_FX_FAILURE - })?; - } - Ok(()) - } - - pub async fn no_files( - &self, - reply: &DirReply<'_, N>, - ) -> SftpOpResult<()> { - reply.send_eof().await.map_err(|err| { - error!("SftpError: {:?}", err); - StatusCode::SSH_FX_FAILURE - }) - } -} diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 86fb6d52..83acdde7 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -3,6 +3,12 @@ name = "sunset-sftp" version = "0.1.1" edition = "2024" +[features] +default = [] + +# Standard library support - enables std helpers +std = [] + [dependencies] sunset = { version = "0.3.0", path = "../" } sunset-async = { path = "../async", version = "0.3" } diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 751b0fa9..92451938 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -69,12 +69,15 @@ pub use sftphandler::SftpHandler; /// Structures and types used to add the details for the target system pub mod server { - // pub use crate::sftpserver::DirEntriesResponseHelpers; pub use crate::sftpserver::DirReply; pub use crate::sftpserver::ReadReply; pub use crate::sftpserver::ReadStatus; pub use crate::sftpserver::SftpOpResult; pub use crate::sftpserver::SftpServer; + #[cfg(feature = "std")] + pub mod helpers { + pub use crate::sftpserver::DirEntriesCollection; + } pub use crate::sftpsink::SftpSink; pub use sunset::sshwire::SSHEncode; } diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index dbf489de..a75b9772 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -7,11 +7,11 @@ use crate::{ proto::{Attrs, Name, ReqId, StatusCode}, }; -use core::marker::PhantomData; -use log::{debug, error, trace}; use sunset::sshwire::SSHEncode; -// use futures::executor::block_on; TODO Deal with the async nature of [`ChanOut`] +use core::marker::PhantomData; +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; /// Result used to store the result of an Sftp Operation pub type SftpOpResult = core::result::Result; @@ -268,3 +268,147 @@ impl<'g, const N: usize> DirReply<'g, N> { self.chan_out.send_status(self.req_id, StatusCode::SSH_FX_EOF, "").await } } + +// TODO Add this to SFTP library only available with std as a global helper +#[cfg(feature = "std")] +use crate::proto::Filename; +#[cfg(feature = "std")] +use std::{ + fs::{DirEntry, Metadata, ReadDir}, + os::{linux::fs::MetadataExt, unix::fs::PermissionsExt}, + time::SystemTime, +}; + +#[cfg(feature = "std")] +/// This is a helper structure to make ReadDir into something manageable for +/// [`DirReply`] +/// +/// WIP: Not stable. It has know issues and most likely it's methods will change +/// +/// BUG: It does not include longname and that may be an issue +#[derive(Debug)] +pub struct DirEntriesCollection { + /// Number of elements + count: u32, + /// Computed length of all the encoded elements + encoded_length: u32, + /// The actual entries. As you can see these are DirEntry. This is a std choice + entries: Vec, +} + +#[cfg(feature = "std")] +impl DirEntriesCollection { + /// Creates this DirEntriesCollection so linux std users do not need to + /// translate `std` directory elements into Sftp structures before sending a response + /// back to the client + pub fn new(dir_iterator: ReadDir) -> Self { + use log::info; + + let mut encoded_length = 0; + + let entries: Vec = dir_iterator + .filter_map(|entry_result| { + let entry = entry_result.ok()?; + let filename = entry.file_name().to_string_lossy().into_owned(); + let name_entry = NameEntry { + filename: Filename::from(filename.as_str()), + _longname: Filename::from(""), + attrs: Self::get_attrs_or_empty(entry.metadata()), + }; + + let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; + let mut sftp_sink = SftpSink::new(&mut buffer); + name_entry.enc(&mut sftp_sink).ok()?; + //TODO remove this unchecked casting + encoded_length += sftp_sink.payload_len() as u32; + Some(entry) + }) + .collect(); + + //TODO remove this unchecked casting + let count = entries.len() as u32; + + info!( + "Processed {} entries, estimated serialized length: {}", + count, encoded_length + ); + + Self { count, encoded_length, entries } + } + + /// Using the provided [`DirReply`] sends a response taking care of + /// composing a SFTP Entry header and sending everything in the right order + /// + /// Returns a [`ReadStatus`] + pub async fn send_response( + &self, + reply: &DirReply<'_, N>, + ) -> SftpOpResult { + self.send_entries_header(reply).await?; + self.send_entries(reply).await?; + Ok(ReadStatus::EndOfFile) + } + /// Sends a header for all the elements in the ReadDir iterator + /// + /// It will take care of counting them and finding the serialized length of each + /// element + async fn send_entries_header( + &self, + reply: &DirReply<'_, N>, + ) -> SftpOpResult<()> { + reply.send_header(self.count, self.encoded_length).await.map_err(|e| { + debug!("Could not send header {e:?}"); + StatusCode::SSH_FX_FAILURE + }) + } + + /// Sends the entries in the ReadDir iterator back to the client + async fn send_entries( + &self, + reply: &DirReply<'_, N>, + ) -> SftpOpResult<()> { + for entry in &self.entries { + let filename = entry.file_name().to_string_lossy().into_owned(); + let attrs = Self::get_attrs_or_empty(entry.metadata()); + let name_entry = NameEntry { + filename: Filename::from(filename.as_str()), + _longname: Filename::from(""), + attrs, + }; + debug!("Sending new item: {:?}", name_entry); + reply.send_item(&name_entry).await.map_err(|err| { + error!("SftpError: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; + } + Ok(()) + } + + fn get_attrs_or_empty( + maybe_metadata: Result, + ) -> Attrs { + maybe_metadata.map(Self::get_attrs).unwrap_or_default() + } + + fn get_attrs(metadata: Metadata) -> Attrs { + let time_to_u32 = |time_result: std::io::Result| { + time_result + .ok()? + .duration_since(SystemTime::UNIX_EPOCH) + .ok()? + .as_secs() + .try_into() + .ok() + }; + + Attrs { + size: Some(metadata.len()), + uid: Some(metadata.st_uid()), + gid: Some(metadata.st_gid()), + permissions: Some(metadata.permissions().mode()), + atime: time_to_u32(metadata.accessed()), + mtime: time_to_u32(metadata.modified()), + ext_count: None, + } + } +} From b27a1e25954235d4878fe0096382793059911e39 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 12 Nov 2025 14:37:06 +1100 Subject: [PATCH 157/393] [skip ci] Refactor proto.rs Name structure and sftpserver trait to remove the `std` dependency It splits the responsibility of encoding/decoding a SSH_FXP_NAME into Name and NameEntry<'a> avoiding the Vector or any fixed memory allocation for the list NameEntry. That required refactoring SftpServer.realpath() and SftpHandler::handle_general_request() to adopt this approach of sending a header and after that one or more NameEntry items Also documenting and reducing the number of warnings --- demo/sftp/std/src/demosftpserver.rs | 10 ++-- demo/sftp/std/src/main.rs | 2 +- sftp/src/lib.rs | 13 +++- sftp/src/proto.rs | 59 +++++++++++++------ sftp/src/sftphandler/sftphandler.rs | 25 ++++---- .../sftphandler/sftpoutputchannelhandler.rs | 22 ++----- sftp/src/sftpserver.rs | 43 ++++++++++---- 7 files changed, 109 insertions(+), 65 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 27ef3420..994626ed 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -163,9 +163,9 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { dir_handle } - fn realpath(&mut self, dir: &str) -> SftpOpResult> { - debug!("finding path for: {:?}", dir); - Ok(Name(vec![NameEntry { + fn realpath(&mut self, dir: &str) -> SftpOpResult> { + info!("finding path for: {:?}", dir); + let name_entry = NameEntry { filename: Filename::from(self.base_path.as_str()), _longname: Filename::from(""), attrs: Attrs { @@ -177,7 +177,9 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { mtime: None, ext_count: None, }, - }])) + }; + debug!("Will return: {:?}", name_entry); + Ok(name_entry) } fn close( diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index ff7cbd4c..eb716fed 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -208,7 +208,7 @@ async fn main(spawner: Spawner) { .filter_level(log::LevelFilter::Warn) .filter_module( "sunset_demo_sftp_std::demosftpserver", - log::LevelFilter::Info, + log::LevelFilter::Debug, ) .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Debug) .filter_module( diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 92451938..874e0b38 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -27,7 +27,7 @@ //! and [write](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) //! - [ ] File [read](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4), //! - [ ] File [stats](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8) -//! - [ ] Directory [Browsing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7) +//! - [x] Directory [Browsing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7) //! //! ## Minimal features for convenient usability //! @@ -49,7 +49,7 @@ #![forbid(unsafe_code)] #![warn(missing_docs)] -// #![no_std] +#![cfg_attr(not(feature = "std"), no_std)] mod opaquefilehandle; mod proto; @@ -67,6 +67,9 @@ mod sftpsource; pub use sftphandler::SftpHandler; /// Structures and types used to add the details for the target system +/// Related to the implementation of the [`server::SftpServer`], which +/// is meant to be instantiated by the user and passed to [`SftpHandler`] +/// and has the task of executing client requests in the underlying system pub mod server { pub use crate::sftpserver::DirReply; @@ -74,8 +77,12 @@ pub mod server { pub use crate::sftpserver::ReadStatus; pub use crate::sftpserver::SftpOpResult; pub use crate::sftpserver::SftpServer; - #[cfg(feature = "std")] + /// Helpers to reduce error prone tasks and hide some details that + /// add complexity when implementing an [`SftpServer`] pub mod helpers { + pub use crate::sftpserver::helpers::*; + + #[cfg(feature = "std")] pub use crate::sftpserver::DirEntriesCollection; } pub use crate::sftpsink::SftpSink; diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 313f6ecd..9c40a367 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -202,44 +202,65 @@ pub struct NameEntry<'a> { pub attrs: Attrs, } +/// This is the encoded length for the Name Sftp Response. +/// +/// This considers the Packet type (1), the Request Id (4) and +/// count of [`NameEntry`] that will follow +/// +/// It excludes the length of [`NameEntry`] explicitly +/// +/// It is defined a single source of truth for what is the length for the +/// encoded [`SftpPacket::Name`] variant +/// +/// See [Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) +pub(crate) const ENCODED_BASE_NAME_SFTP_PACKET_LENGTH: u32 = 9; + // TODO Will a Vector be an issue for no_std? // Maybe we should migrate this to heapless::Vec and let the user decide // the number of elements via features flags? +/// This is the first part of the `SSH_FXP_NAME` response. It includes +/// only the count of [`NameEntry`] items that follow this Name +/// +/// After encoding or decoding [`Name`], [`NameEntry`] must be encoded or +/// decoded `count` times /// A collection of [`NameEntry`] used for [ssh_fxp_name responses](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7). #[derive(Debug)] -pub struct Name<'a>(pub Vec>); +// pub struct Name<'a>(pub Vec>); +pub struct Name { + /// Number of [`NameEntry`] items that follow this Name + pub count: u32, +} -impl<'a: 'de, 'de> SSHDecode<'de> for Name<'a> -where - 'de: 'a, -{ +impl<'de> SSHDecode<'de> for Name { fn dec(s: &mut S) -> WireResult where S: SSHSource<'de>, { - let count = u32::dec(s)? as usize; + let count = u32::dec(s)? as u32; - let mut names = Vec::with_capacity(count); + // let mut names = Vec::with_capacity(count); - for _ in 0..count { - names.push(NameEntry::dec(s)?); - } + // for _ in 0..count { + // names.push(NameEntry::dec(s)?); + // } - Ok(Name(names)) + Ok(Name { count }) } } -impl<'a> SSHEncode for Name<'a> { +impl SSHEncode for Name { fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { - (self.0.len() as u32).enc(s)?; + self.count.enc(s) + // (self.0.len() as u32).enc(s)?; - for element in self.0.iter() { - element.enc(s)?; - } - Ok(()) + // for element in self.0.iter() { + // element.enc(s)?; + // } + // Ok(()) } } +// TODO: Is this really necessary? #[derive(Debug, SSHEncode, SSHDecode)] pub struct ResponseAttributes { pub attrs: Attrs, @@ -314,6 +335,8 @@ impl SSHEncode for StatusCode { } } +// TODO: Implement extensions. Low in priority +/// Provided to provide a mechanism to implement extensions #[derive(Debug, SSHEncode, SSHDecode)] pub struct ExtPair<'a> { pub name: &'a str, @@ -819,7 +842,7 @@ sftpmessages! [ (101, Status, Status<'a>, "ssh_fxp_status"), (102, Handle, Handle<'a>, "ssh_fxp_handle"), (103, Data, Data<'a>, "ssh_fxp_data"), - (104, Name, Name<'a>, "ssh_fxp_name"), + (104, Name, Name, "ssh_fxp_name"), }, ]; diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index ea70cdc9..aaa6e68f 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -4,10 +4,10 @@ use crate::error::SftpError; use crate::handles::OpaqueFileHandle; use crate::proto::{ self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, - SftpPacket, Status, StatusCode, + SftpPacket, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; -use crate::server::{DirReply, ReadStatus, SftpOpResult, SftpSink}; +use crate::server::DirReply; use crate::sftperror::SftpResult; use crate::sftphandler::sftpoutputchannelhandler::{ SftpOutputPipe, SftpOutputProducer, @@ -17,7 +17,7 @@ use crate::sftpsource::SftpSource; use embassy_futures::select::select; use sunset::Error as SunsetError; -use sunset::sshwire::{SSHEncode, SSHSource, WireError}; +use sunset::sshwire::{SSHSource, WireError}; use sunset_async::ChanInOut; use core::u32; @@ -493,6 +493,7 @@ where self.process(&buffer_in[0..lr], &output_producer).await?; } + #[allow(unreachable_code)] SftpResult::Ok(()) }; match select(processing_loop, output_consumer_loop).await { @@ -530,15 +531,15 @@ where return Err(SftpError::AlreadyInitialized); } SftpPacket::PathInfo(req_id, path_info) => { - let a_name = file_server.realpath(path_info.path.as_str()?)?; - - let response = SftpPacket::Name(req_id, a_name); - debug!( - "Request Id {:?}. Encoding response: {:?}", - &req_id, &response - ); - - output_producer.send_packet(&response).await?; + let dir_reply = DirReply::new(req_id, output_producer); + let name_entry = file_server.realpath(path_info.path.as_str()?)?; + + let encoded_len = + crate::sftpserver::helpers::get_name_entry_len(&name_entry)?; + debug!("PathInfo encoded length: {:?}", encoded_len); + trace!("PathInfo Response content: {:?}", encoded_len); + dir_reply.send_header(1, encoded_len).await?; + dir_reply.send_item(&name_entry).await?; } SftpPacket::Open(req_id, open) => { match file_server.open(open.filename.as_str()?, &open.attrs) { diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index ad2e8a30..967c24c7 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -74,14 +74,14 @@ impl<'a, const N: usize> SftpOutputConsumer<'a, N> { let mut buf = [0u8; N]; loop { let rl = self.reader.read(&mut buf).await; - let mut total = 0; + let mut _total = 0; { let mut lock = self.counter.lock().await; *lock += rl; - total = *lock; + _total = *lock; } - debug!("Output Consumer: Reads {rl} bytes. Total {total}"); + debug!("Output Consumer: Reads {rl} bytes. Total {_total}"); if rl > 0 { self.ssh_chan_out.write_all(&buf[..rl]).await?; debug!("Output Consumer: Written {:?} bytes ", &buf[..rl].len()); @@ -110,16 +110,6 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { Ok(()) } - /// Sends the data encoded in the provided [`SftpSink`] without including - /// the size. - /// - /// Use this when you are sending chunks of data after a valid header - pub async fn send_payload(&self, sftp_sink: &SftpSink<'_>) -> SftpResult<()> { - let buf = sftp_sink.payload_slice(); - Self::send_buffer(&self.writer, &buf, &self.counter).await; - Ok(()) - } - /// Simplifies the task of sending a status response to the client. pub async fn send_status( &self, @@ -153,14 +143,14 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { buf: &[u8], counter: &CounterMutex, ) { - let mut total = 0; + let mut _total = 0; { let mut lock = counter.lock().await; *lock += buf.len(); - total = *lock; + _total = *lock; } - debug!("Output Producer: Sends {:?} bytes. Total {total}", buf.len()); + debug!("Output Producer: Sends {:?} bytes. Total {_total}", buf.len()); trace!("Output Producer: Sending buffer {:?}", buf); // writer.write_all(buf); // ??? error[E0596]: cannot borrow `*writer` as mutable, as it is behind a `&` reference diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index a75b9772..5204c20d 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,10 +1,12 @@ use crate::error::SftpResult; -use crate::proto::{MAX_NAME_ENTRY_SIZE, NameEntry}; +use crate::proto::{ + ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, MAX_NAME_ENTRY_SIZE, NameEntry, +}; use crate::server::SftpSink; use crate::sftphandler::SftpOutputProducer; use crate::{ handles::OpaqueFileHandle, - proto::{Attrs, Name, ReqId, StatusCode}, + proto::{Attrs, ReqId, StatusCode}, }; use sunset::sshwire::SSHEncode; @@ -133,7 +135,7 @@ where } /// Provides the real path of the directory specified - fn realpath(&mut self, dir: &str) -> SftpOpResult> { + fn realpath(&mut self, dir: &str) -> SftpOpResult> { log::error!("SftpServer RealPath operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } @@ -206,6 +208,8 @@ pub struct DirReply<'g, const N: usize> { } impl<'g, const N: usize> DirReply<'g, N> { + // const ENCODED_NAME_SFTP_PACKET_LENGTH: u32 = 9; + /// New instances can only be created within the crate. Users can only /// use other public methods to use it. pub(crate) fn new( @@ -221,24 +225,23 @@ impl<'g, const N: usize> DirReply<'g, N> { /// length of all these [`NameEntry`] items pub async fn send_header( &self, - get_count: u32, - get_encoded_len: u32, + count: u32, + items_encoded_len: u32, ) -> SftpResult<()> { debug!( "I will send the header here for request id {:?}: count = {:?}, length = {:?}", - self.req_id, get_count, get_encoded_len + self.req_id, count, items_encoded_len ); let mut s = [0u8; N]; let mut sink = SftpSink::new(&mut s); - get_encoded_len.enc(&mut sink)?; - 104u8.enc(&mut sink)?; // TODO Replace hack with - self.req_id.enc(&mut sink)?; - let encoded_name_sftp_packet_length: u32 = 9; // We need to consider the packet type, Id and count fields // This way I collect data required for the header and collect // valid entries into a vector (only std) - (get_count + encoded_name_sftp_packet_length).enc(&mut sink)?; + (items_encoded_len + ENCODED_BASE_NAME_SFTP_PACKET_LENGTH).enc(&mut sink)?; + 104u8.enc(&mut sink)?; // TODO Replace hack with + self.req_id.enc(&mut sink)?; + count.enc(&mut sink)?; let payload = sink.payload_slice(); debug!( "Sending header: len = {:?}, content = {:?}", @@ -269,6 +272,24 @@ impl<'g, const N: usize> DirReply<'g, N> { } } +pub mod helpers { + use crate::{ + error::SftpResult, + proto::{MAX_NAME_ENTRY_SIZE, NameEntry}, + server::SftpSink, + }; + + use sunset::sshwire::SSHEncode; + + /// Helper function to get the length of a [`NameEntry`] + pub fn get_name_entry_len(name_entry: &NameEntry<'_>) -> SftpResult { + let mut buf = [0u8; MAX_NAME_ENTRY_SIZE]; + let mut temp_sink = SftpSink::new(&mut buf); + name_entry.enc(&mut temp_sink)?; + Ok(temp_sink.payload_len() as u32) + } +} + // TODO Add this to SFTP library only available with std as a global helper #[cfg(feature = "std")] use crate::proto::Filename; From 021e4cd49271fad75a92cc3307ffc4a960dd211a Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 12 Nov 2025 15:03:45 +1100 Subject: [PATCH 158/393] Basic Features Directory read is now complete sunset-sftp and sunset-demo-sftp-std crates version have been increased and now match. Documentation has been updated, An Producer Consumer subsystem has been introduced to allow sending responses exceeding the output buffer length without introducing lifetimes and borrowing issues. proto.rs has been refactored to eliminate all std dependencies. helper structures and modules have been added to assist the SftpServer` implementer. a new crate feature `std` has been introduced to allow accessing std helper structures that reduce the exposure of sftp protocol details to the `SftpServer` implementer. The std example has been refactored. While still quite complex it delegates some pretty specific tasks to the std helpers. --- demo/sftp/std/Cargo.toml | 2 +- sftp/Cargo.toml | 2 +- sftp/src/lib.rs | 2 +- sftp/src/sftphandler/sftphandler.rs | 5 +++++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/demo/sftp/std/Cargo.toml b/demo/sftp/std/Cargo.toml index 8a3eaa5f..a2f08d6f 100644 --- a/demo/sftp/std/Cargo.toml +++ b/demo/sftp/std/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sunset-demo-sftp-std" -version = "0.1.0" +version = "0.1.2" edition = "2021" [dependencies] diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 83acdde7..704a9a84 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sunset-sftp" -version = "0.1.1" +version = "0.1.2" edition = "2024" [features] diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 874e0b38..6f518fd1 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -25,9 +25,9 @@ //! - [x] [Canonicalizing the Server-Side Path Name](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.11) support //! - [x] [Open, close](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3) //! and [write](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) +//! - [x] Directory [Browsing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7) //! - [ ] File [read](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4), //! - [ ] File [stats](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8) -//! - [x] Directory [Browsing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7) //! //! ## Minimal features for convenient usability //! diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index aaa6e68f..42a91c39 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -91,6 +91,11 @@ enum FragmentedRequestState { /// It will delegate request to an [`crate::sftpserver::SftpServer`] /// implemented by the library /// user taking into account the local system details. +/// +/// The compiler time constant `BUFFER_OUT_SIZE` is used to define the +/// size of the output buffer for the subsystem [`Embassy-sync::pipe`] used +/// to send responses safely across the instantiated structure. +/// pub struct SftpHandler<'a, T, S, const BUFFER_OUT_SIZE: usize> where T: OpaqueFileHandle, From e6b1c6aa2a909d8398f0f0ed8553cd654d4dbc5f Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Tue, 9 Sep 2025 12:02:31 +1000 Subject: [PATCH 159/393] Add the necessary structs/types for env var SendEnv (and avoid WARN - Unknown channel req type 'env')... that's according to RFC 4254, but we'll deem more env vars as 'safe to pass' than just LANG, TERM, and DISPLAY. [src/serve.rs:98] "Got ENV request" = "Got ENV request" [src/serve.rs:99] a.name()? = "LANG" [src/serve.rs:100] a.value()? = "C.UTF-8" Co-authored-by: Marko Malenic Co-authored-by: Julio Beltran Ortega --- Cargo.toml | 1 - demo/common/src/config.rs | 2 +- demo/common/src/server.rs | 1 + src/channel.rs | 24 +++++++++++++ src/event.rs | 76 ++++++++++++++++++++++++++++++++++++++- src/packets.rs | 14 ++++++-- src/runner.rs | 15 ++++++++ 7 files changed, 128 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 79bbcbbf..69ff1cec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,6 @@ mlkem = ["dep:ml-kem"] openssh-key = ["ssh-key"] # implements embedded_io::Error for sunset::Error embedded-io = ["dep:embedded-io"] - # Arbitrary for fuzzing. std is required for derive(Arbitrary) arbitrary = ["dep:arbitrary", "std"] diff --git a/demo/common/src/config.rs b/demo/common/src/config.rs index 4e7cf8ef..a1df9780 100644 --- a/demo/common/src/config.rs +++ b/demo/common/src/config.rs @@ -178,7 +178,7 @@ where let ad = Ipv4Address::from_bits(ad); let prefix = SSHDecode::dec(s)?; if prefix > 32 { - // emabassy panics, so test it here + // embassy panics, so test it here return Err(WireError::PacketWrong); } let gw: Option = dec_option(s)?; diff --git a/demo/common/src/server.rs b/demo/common/src/server.rs index c0c79040..2207fef7 100644 --- a/demo/common/src/server.rs +++ b/demo/common/src/server.rs @@ -118,6 +118,7 @@ impl DemoCommon { ServEvent::PubkeyAuth(a) => self.handle_pubkey(a), ServEvent::OpenSession(a) => self.open_session(a), ServEvent::SessionPty(a) => a.succeed(), + ServEvent::Environment(a) => a.succeed(), ServEvent::SessionSubsystem(a) => { info!("Ignored request for subsystem '{}'", a.command()?); Ok(()) diff --git a/src/channel.rs b/src/channel.rs index 7fcf6de5..bef70f9c 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -569,6 +569,27 @@ impl Channels { _ => Err(Error::bug()), } } + + pub fn fetch_env_name<'p>(&self, p: &Packet<'p>) -> Result> { + match p { + Packet::ChannelRequest(ChannelRequest { + req: ChannelReqType::Environment(packets::Environment { name, .. }), + .. + }) => Ok(name.clone()), + _ => Err(Error::bug()), + } + } + + pub fn fetch_env_value<'p>(&self, p: &Packet<'p>) -> Result> { + match p { + Packet::ChannelRequest(ChannelRequest { + req: + ChannelReqType::Environment(packets::Environment { name: _, value }), + .. + }) => Ok(value.clone()), + _ => Err(Error::bug()), + } + } } #[derive(Debug, Clone, Copy)] @@ -900,6 +921,9 @@ impl Channel { ChannelReqType::Pty(_) => { Ok(DispatchEvent::ServEvent(ServEventId::SessionPty { num })) } + ChannelReqType::Environment(_) => { + Ok(DispatchEvent::ServEvent(ServEventId::Environment { num })) + } _ => { if let ChannelReqType::Unknown(u) = &p.req { warn!("Unknown channel req type \"{}\"", u) diff --git a/src/event.rs b/src/event.rs index b185e912..106825e0 100644 --- a/src/event.rs +++ b/src/event.rs @@ -300,6 +300,8 @@ pub enum ServEvent<'g, 'a> { /// /// TODO details SessionPty(ServPtyRequest<'g, 'a>), + /// Server has received one environment variable + Environment(ServEnvironmentRequest<'g, 'a>), /// The SSH session is no longer running #[allow(unused)] @@ -325,6 +327,7 @@ impl Debug for ServEvent<'_, '_> { Self::SessionExec(_) => "SessionExec", Self::SessionSubsystem(_) => "SessionSubsystem", Self::SessionPty(_) => "SessionPty", + Self::Environment(_) => "Environment", Self::Defunct => "Defunct", Self::PollAgain => "PollAgain", }; @@ -621,7 +624,7 @@ impl<'g, 'a> ServExecRequest<'g, 'a> { self.raw_command()?.as_str() } - pub fn raw_command(&self) -> Result> { + fn raw_command(&self) -> Result> { self.runner.fetch_servcommand() } @@ -723,6 +726,69 @@ impl Drop for ServPtyRequest<'_, '_> { } } +/// An environment variable request +/// +pub struct ServEnvironmentRequest<'g, 'a> { + runner: &'g mut Runner<'a, Server>, + num: ChanNum, + done: bool, +} + +impl<'g, 'a> ServEnvironmentRequest<'g, 'a> { + fn new(runner: &'g mut Runner<'a, Server>, num: ChanNum) -> Self { + Self { runner, num, done: false } + } + + /// Indicate that the request succeeded. + /// + /// Note that if the peer didn't request a reply, this call + /// will not do anything. + pub fn succeed(mut self) -> Result<()> { + self.done = true; + self.runner.resume_chanreq(true) + } + + /// Indicate that the request failed. + /// + /// Note that if the peer didn't request a reply, this call + /// will not do anything. + /// Does not need to be called explicitly, also occurs on drop without `accept()` + pub fn fail(mut self) -> Result<()> { + self.done = true; + self.runner.resume_chanreq(false) + } + + /// Return the associated channel number. + /// + /// This will correspond to a `ChanHandle::num()` + /// from a previous [`ServOpenSession`] event. + pub fn channel(&self) -> ChanNum { + self.num + } + + /// Retrieve the name of the environment variable (from NAME=VALUE pair). + pub fn name(&self) -> Result<&str> { + self.raw_name()?.as_str() + } + + /// Retrieve the raw name of the environment variable. + fn raw_name(&self) -> Result> { + self.runner.fetch_env_name() + } + + /// Retrieve the value of the environment variable (from NAME=VALUE pair). + pub fn value(&self) -> Result<&str> { + self.raw_value()?.as_str() + } + + /// Retrieve the raw value of the environment variable. + fn raw_value(&self) -> Result> { + self.runner.fetch_env_value() + } + + // TODO: does the app care about wantreply? +} + // Only small values should be stored inline. // Larger state is retrieved from the current packet via Runner::fetch_*() #[derive(Debug, Clone)] @@ -748,6 +814,9 @@ pub(crate) enum ServEventId { SessionPty { num: ChanNum, }, + Environment { + num: ChanNum, + }, #[allow(unused)] Defunct, // TODO: @@ -801,6 +870,10 @@ impl ServEventId { debug_assert!(matches!(p, Some(Packet::ChannelRequest(_)))); Ok(ServEvent::SessionPty(ServPtyRequest::new(runner, num))) } + Self::Environment { num } => { + debug_assert!(matches!(p, Some(Packet::ChannelRequest(_)))); + Ok(ServEvent::Environment(ServEnvironmentRequest::new(runner, num))) + } Self::Defunct => Ok(ServEvent::Defunct), } } @@ -818,6 +891,7 @@ impl ServEventId { | Self::SessionShell { .. } | Self::SessionExec { .. } | Self::SessionSubsystem { .. } + | Self::Environment { .. } | Self::SessionPty { .. } => true, } } diff --git a/src/packets.rs b/src/packets.rs index fa55c812..8026a035 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -2,7 +2,7 @@ //! //! A [`Packet`] can be encoded/decoded to the //! SSH Binary Packet Protocol using [`sshwire`]. -//! SSH packet format is described in [RFC4253](https://tools.ietf.org/html/rfc5643) SSH Transport +//! SSH packet format is described in [RFC4253](https://tools.ietf.org/html/rfc4253#section-6) SSH Transport #[allow(unused_imports)] use { @@ -711,6 +711,8 @@ pub enum ChannelReqType<'a> { Subsystem(Subsystem<'a>), #[sshwire(variant = "window-change")] WinChange(WinChange), + #[sshwire(variant = "env")] + Environment(Environment<'a>), #[sshwire(variant = "signal")] Signal(Signal<'a>), #[sshwire(variant = "exit-status")] @@ -725,7 +727,6 @@ pub enum ChannelReqType<'a> { // Other requests that aren't implemented at present: // auth-agent-req@openssh.com // x11-req - // env // xon-xoff #[sshwire(unknown)] Unknown(Unknown<'a>), @@ -766,6 +767,15 @@ pub struct WinChange { pub height: u32, } +/// An environment variable +#[derive(Debug, SSHEncode, SSHDecode)] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +pub struct Environment<'a> { + // pub wants_reply: bool, // TODO: needed? + pub name: TextString<'a>, + pub value: TextString<'a>, +} + /// A unix signal channel request #[derive(Debug, SSHEncode, SSHDecode)] #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] diff --git a/src/runner.rs b/src/runner.rs index 6cfcb9b1..3b22977a 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -765,6 +765,7 @@ impl<'a, CS: CliServ> Runner<'a, CS> { | DispatchEvent::ServEvent(ServEventId::SessionExec { .. }) | DispatchEvent::ServEvent(ServEventId::SessionSubsystem { .. }) | DispatchEvent::ServEvent(ServEventId::SessionPty { .. }) + | DispatchEvent::ServEvent(ServEventId::Environment { .. }) )); } @@ -787,6 +788,20 @@ impl<'a, CS: CliServ> Runner<'a, CS> { let p = self.conn.packet(payload)?; self.conn.channels.fetch_servcommand(&p) } + + pub(crate) fn fetch_env_name(&self) -> Result> { + Self::check_chanreq(&self.resume_event); + let (payload, _seq) = self.traf_in.payload().trap()?; + let p = self.conn.packet(payload)?; + self.conn.channels.fetch_env_name(&p) + } + + pub(crate) fn fetch_env_value(&self) -> Result> { + Self::check_chanreq(&self.resume_event); + let (payload, _seq) = self.traf_in.payload().trap()?; + let p = self.conn.packet(payload)?; + self.conn.channels.fetch_env_value(&p) + } } /// Sets a waker, waking any existing waker From 76dc4f735d5e72c39664c924a8b1bcbd612e7868 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 09:41:48 +1100 Subject: [PATCH 160/393] [skip ci] Extending SftpSource and Tests Now we can peak the total packet length and also if the contained packet fits within the buffer. Tests for all of that --- demo/sftp/std/src/main.rs | 2 +- sftp/src/proto.rs | 17 +++-- sftp/src/sftpsource.rs | 134 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 142 insertions(+), 11 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index eb716fed..7ddf09a8 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -205,7 +205,7 @@ async fn listen( #[embassy_executor::main] async fn main(spawner: Spawner) { env_logger::builder() - .filter_level(log::LevelFilter::Warn) + .filter_level(log::LevelFilter::Debug) .filter_module( "sunset_demo_sftp_std::demosftpserver", log::LevelFilter::Debug, diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 9c40a367..09cbbff2 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -13,17 +13,26 @@ use paste::paste; #[allow(unused)] pub const SFTP_MINIMUM_PACKET_LEN: usize = 9; +#[allow(unused)] +pub const SFTP_FIELD_LEN_INDEX: usize = 0; +/// SFTP packets length field us u32 +#[allow(unused)] +pub const SFTP_FIELD_LEN_LENGTH: usize = 4; /// SFTP packets have the packet type after a u32 length field #[allow(unused)] pub const SFTP_FIELD_ID_INDEX: usize = 4; /// SFTP packets ID length is 1 byte -// pub const SFTP_FIELD_ID_LEN: usize = 1; +#[allow(unused)] +pub const SFTP_FIELD_ID_LEN: usize = 1; /// SFTP packets start with the length field + +/// SFTP packets have the packet request id after field id #[allow(unused)] -pub const SFTP_FIELD_LEN_INDEX: usize = 0; -/// SFTP packets length field us u32 +pub const SFTP_FIELD_REQ_ID_INDEX: usize = 5; +/// SFTP packets ID length is 1 byte #[allow(unused)] -pub const SFTP_FIELD_LEN_LENGTH: usize = 4; +pub const SFTP_FIELD_REQ_ID_LEN: usize = 4; +/// SFTP packets start with the length field // SSH_FXP_WRITE SFTP Packet definition used to decode long packets that do not fit in one buffer diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 1a29a149..0266dbd6 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -2,7 +2,8 @@ use crate::error::{SftpError, SftpResult}; use crate::handles::OpaqueFileHandle; use crate::proto::{ ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, - SFTP_MINIMUM_PACKET_LEN, SFTP_WRITE_REQID_INDEX, SftpNum, + SFTP_FIELD_REQ_ID_INDEX, SFTP_FIELD_REQ_ID_LEN, SFTP_MINIMUM_PACKET_LEN, + SFTP_WRITE_REQID_INDEX, SftpNum, }; use crate::protocol::FileHandle; use crate::sftphandler::PartialWriteRequestTracker; @@ -50,7 +51,6 @@ impl<'de> SftpSource<'de> { debug!("New source with content: : {:?}", buffer); SftpSource { buffer: buffer, index: 0 } } - /// Peaks the buffer for packet type [`SftpNum`]. This does not advance /// the reading index /// @@ -58,7 +58,7 @@ impl<'de> SftpSource<'de> { /// `dec(s)` would fail /// /// **Warning**: will only work in well formed packets, in other case - /// the result will contain garbage + /// the result will contains garbage pub(crate) fn peak_packet_type(&self) -> WireResult { if self.buffer.len() <= SFTP_FIELD_ID_INDEX { error!( @@ -72,13 +72,16 @@ impl<'de> SftpSource<'de> { } } - /// Peaks the buffer for packet length. This does not advance the reading index + /// Peaks the buffer for packet length field. This does not advance the reading index /// /// Useful to observe the packet fields in special conditions where a `dec(s)` /// would fail /// + /// Use `peak_total_packet_len` instead if you want to also consider the the + /// length field + /// /// **Warning**: will only work in well formed packets, in other case the result - /// will contain garbage + /// will contains garbage pub(crate) fn peak_packet_len(&self) -> WireResult { if self.buffer.len() < SFTP_FIELD_LEN_INDEX + SFTP_FIELD_LEN_LENGTH { Err(WireError::RanOut) @@ -92,6 +95,32 @@ impl<'de> SftpSource<'de> { } } + /// Peaks the packet in the source to obtain a total packet length, which + /// considers the length of the length field itself. For the packet length field + /// use [`peak_packet_len()`] + /// + /// This does not advance the reading index + /// + /// This does not consider the length field itself + /// Useful to observe the packet fields in special conditions where a `dec(s)` + /// would fail + /// + /// **Warning**: will only work in well formed packets, in other case the result + /// will contains garbage + pub(crate) fn peak_total_packet_len(&self) -> WireResult { + Ok(self.peak_packet_len()? + SFTP_FIELD_LEN_LENGTH as u32) + } + + // TODO: Test This for correctness + /// Compares the total source capacity and the peaked packet length + /// plus the length field length itself to find out if the packet fit + /// in the source + /// **Warning**: will only work in well formed packets, in other case + /// the result will contains garbage + pub fn packet_fits(&self) -> WireResult { + Ok(self.buffer.len() >= self.peak_total_packet_len()? as usize) + } + /// Assuming that the buffer contains a [`proto::Write`] request packet initial /// bytes and not its totality: /// @@ -104,7 +133,7 @@ impl<'de> SftpSource<'de> { /// - [`PartialWriteRequestTracker`] to handle subsequent portions of the request /// /// **Warning**: will only work in well formed write packets, in other case - /// the result will contain garbage + /// the result will contains garbage pub(crate) fn dec_packet_partial_write_content_and_get_tracker< T: OpaqueFileHandle, >( @@ -159,4 +188,97 @@ impl<'de> SftpSource<'de> { ) -> WireResult> { Ok(BinString(self.take(len)?)) } + + pub fn peak_packet_req_id(&self) -> WireResult { + if self.buffer.len() < SFTP_FIELD_REQ_ID_INDEX + SFTP_FIELD_REQ_ID_LEN { + Err(WireError::RanOut) + } else { + let bytes: [u8; 4] = self.buffer[SFTP_FIELD_REQ_ID_INDEX + ..SFTP_FIELD_REQ_ID_INDEX + SFTP_FIELD_LEN_LENGTH] + .try_into() + .expect("slice length mismatch"); + + Ok(u32::from_be_bytes(bytes)) + } + } + + /// Discards the first elements of the + pub fn consume_first(&mut self, len: usize) -> WireResult<()> { + if len > self.buffer.len() { + Err(WireError::RanOut) + } else { + self.index = len; + Ok(()) + } + } +} + +#[cfg(test)] +mod local_tests { + use super::*; + + fn status_buffer() -> [u8; 27] { + let expected_status_packet_slice: [u8; 27] = [ + 0, 0, 0, 23, // Packet len + 101, // Packet type + 0, 0, 0, 16, // ReqId + 0, 0, 0, 1, // Status code: SSH_FX_EOF + 0, 0, 0, 1, // string message length + 65, // string message content + 0, 0, 0, 5, // string lang length + 101, 110, 45, 85, 83, // string lang content + ]; + expected_status_packet_slice + } + + #[test] + fn peaking_len() { + let buffer_status = status_buffer(); + let sink = SftpSource::new(&buffer_status); + + let read_packet_len = sink.peak_packet_len().unwrap(); + let original_packet_len = 23u32; + assert_eq!(original_packet_len, read_packet_len); + } + #[test] + fn peaking_total_len() { + let buffer_status = status_buffer(); + let sink = SftpSource::new(&buffer_status); + + let read_total_packet_len = sink.peak_total_packet_len().unwrap(); + let original_total_packet_len = 23u32 + 4u32; + assert_eq!(original_total_packet_len, read_total_packet_len); + } + + #[test] + fn peaking_type() { + let buffer_status = status_buffer(); + let sink = SftpSource::new(&buffer_status); + let read_packet_type = sink.peak_packet_type().unwrap(); + let original_packet_type = SftpNum::from(101u8); + assert_eq!(original_packet_type, read_packet_type); + } + #[test] + fn peaking_req_id() { + let buffer_status = status_buffer(); + let sink = SftpSource::new(&buffer_status); + let read_req_id = sink.peak_packet_req_id().unwrap(); + let original_req_id = 16u32; + assert_eq!(original_req_id, read_req_id); + } + + #[test] + fn packet_does_fit() { + let buffer_status = status_buffer(); + let sink = SftpSource::new(&buffer_status); + assert_eq!(true, sink.packet_fits().unwrap()); + } + + #[test] + fn packet_does_not_fit() { + let buffer_status = status_buffer(); + let no_room_buffer = &buffer_status[..buffer_status.len() - 2]; + let sink = SftpSource::new(no_room_buffer); + assert_eq!(false, sink.packet_fits().unwrap()); + } } From 5796819dbe4342ca6e132964cfa4d3eb8b719dd7 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 09:48:38 +1100 Subject: [PATCH 161/393] [skip ci] SftpOutputPipe checks if already splits --- sftp/src/sftphandler/sftphandler.rs | 35 +------------------ .../sftphandler/sftpoutputchannelhandler.rs | 14 +++++--- 2 files changed, 11 insertions(+), 38 deletions(-) diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 42a91c39..e5e2f7d9 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -52,39 +52,6 @@ enum FragmentedRequestState { ProcessingLongRequest, } -// // TODO Generalize this to allow other request types -// /// Used to keep record of a long SFTP Write request that does not fit in -// /// receiving buffer and requires processing in batches -// #[derive(Debug)] -// pub struct PartialWriteRequestTracker { -// req_id: ReqId, -// opaque_handle: T, -// remain_data_len: u32, -// remain_data_offset: u64, -// } - -// impl PartialWriteRequestTracker { -// /// Creates a new [`PartialWriteRequestTracker`] -// pub fn new( -// req_id: ReqId, -// opaque_handle: T, -// remain_data_len: u32, -// remain_data_offset: u64, -// ) -> WireResult { -// Ok(PartialWriteRequestTracker { -// req_id, -// opaque_handle: opaque_handle, -// remain_data_len, -// remain_data_offset, -// }) -// } -// /// Returns the opaque file handle associated with the request -// /// tracked -// pub fn get_opaque_file_handle(&self) -> T { -// self.opaque_handle.clone() -// } -// } - /// Process the raw buffers in and out from a subsystem channel decoding /// request and encoding responses /// @@ -481,7 +448,7 @@ where let mut sftp_output_pipe = SftpOutputPipe::::new(); let (mut output_consumer, output_producer) = - sftp_output_pipe.split(chan_out); + sftp_output_pipe.split(chan_out)?; let output_consumer_loop = output_consumer.receive_task(); diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 967c24c7..85d051f8 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -1,4 +1,4 @@ -use crate::error::SftpResult; +use crate::error::{SftpError, SftpResult}; use crate::proto::{ReqId, SftpPacket, Status, StatusCode}; use crate::server::SftpSink; @@ -17,6 +17,7 @@ pub struct SftpOutputPipe { pipe: Pipe, counter_send: CounterMutex, counter_recv: CounterMutex, + splitted: bool, } /// M: SunsetSunsetRawMutex @@ -33,6 +34,7 @@ impl SftpOutputPipe { pipe: Pipe::new(), counter_send: Mutex::::new(0), counter_recv: Mutex::::new(0), + splitted: false, } } @@ -50,12 +52,16 @@ impl SftpOutputPipe { pub fn split<'a>( &'a mut self, ssh_chan_out: ChanOut<'a>, - ) -> (SftpOutputConsumer<'a, N>, SftpOutputProducer<'a, N>) { + ) -> SftpResult<(SftpOutputConsumer<'a, N>, SftpOutputProducer<'a, N>)> { + if self.splitted { + return Err(SftpError::AlreadyInitialized); + } + self.splitted = true; let (reader, writer) = self.pipe.split(); - ( + Ok(( SftpOutputConsumer { reader, ssh_chan_out, counter: &self.counter_recv }, SftpOutputProducer { writer, counter: &self.counter_send }, - ) + )) } } From 8000b21e21e51fd309ace5d4e07d4e9d675c45b9 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 09:49:28 +1100 Subject: [PATCH 162/393] removed TODO, not a good idea --- sftp/src/sftphandler/partialwriterequesttracker.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/sftphandler/partialwriterequesttracker.rs b/sftp/src/sftphandler/partialwriterequesttracker.rs index 72acbfe4..38ea4f6a 100644 --- a/sftp/src/sftphandler/partialwriterequesttracker.rs +++ b/sftp/src/sftphandler/partialwriterequesttracker.rs @@ -55,6 +55,6 @@ impl PartialWriteRequestTracker { } pub(crate) fn get_req_id(&self) -> ReqId { - self.req_id.clone() // TODO reference? + self.req_id.clone() } } From 79d8c9b37c712585ad0c5ff3a61aa7c930a57e97 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 10:08:19 +1100 Subject: [PATCH 163/393] [skip ci] intercepting incomplete UnknownPacket in decode request to help flushing it It is important that we flush the rest of an incomplete unknown packet. So until it is completed, we will return a RanOut so we can read it full and then flush it --- sftp/src/proto.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 09cbbff2..9dd5fa17 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,3 +1,5 @@ +use crate::sftpsource::SftpSource; + use sunset::sshwire::{ BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, WireResult, @@ -277,7 +279,7 @@ pub struct ResponseAttributes { // Requests/Responses data types -#[derive(Debug, SSHEncode, SSHDecode, Clone, Copy)] +#[derive(Debug, SSHEncode, SSHDecode, Clone, Copy, PartialEq)] pub struct ReqId(pub u32); /// For more information see [Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) @@ -508,7 +510,7 @@ macro_rules! sftpmessages { ) => { paste! { /// Represent a subset of the SFTP packet types defined by draft-ietf-secsh-filexfer-02 - #[derive(Debug, Clone, FromPrimitive, SSHEncode)] + #[derive(Debug, Clone, PartialEq, FromPrimitive, SSHEncode)] #[repr(u8)] #[allow(non_camel_case_types)] pub enum SftpNum { @@ -729,9 +731,9 @@ macro_rules! sftpmessages { /// Decode a response. /// /// Used by a SFTP client. Does not include the length field. - pub fn decode_response<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> + pub fn decode_response<'de>(s: &mut SftpSource<'de>) -> WireResult<(ReqId, Self)> where - S: SSHSource<'de>, + // S: SftpSource<'de>, 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes 'de: 'a { @@ -753,9 +755,9 @@ macro_rules! sftpmessages { /// Used by a SFTP server. Does not include the length field. /// /// It will fail if the received packet is a response, no valid or incomplete packet - pub fn decode_request<'de, S>(s: &mut S) -> WireResult + pub fn decode_request<'de>(s: &mut SftpSource<'de>) -> WireResult where - S: SSHSource<'de>, + // S: SftpSource<'de>, 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes 'de: 'a { @@ -774,7 +776,11 @@ macro_rules! sftpmessages { } }, Err(e) => { - Err(e) + match e { + WireError::UnknownPacket{..} if !s.packet_fits()? => Err(WireError::RanOut), + _ => Err(e) + } + } } } From bdb508ab0d200bbc6448d2b40dfbdfb50dfedff3 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 10:11:26 +1100 Subject: [PATCH 164/393] Handling properly unknown packets This way: - We send the right ReqId for Unsupported status - Flush out the complete packet to avoid desynchronizing packet decoding --- sftp/src/sftphandler/sftphandler.rs | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index e5e2f7d9..fc94392d 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -212,6 +212,25 @@ where self.state = SftpHandleState::Idle; } Err(e) => match e { + WireError::UnknownPacket { number } => { + warn!( + "Unknown packet: packetId = {:?}. Will flush \ + its length and send unsupported back", + number + ); + + let req_id = ReqId(source.peak_packet_req_id()?); + let len = + source.peak_total_packet_len()? as usize; + source.consume_first(len)?; + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_OP_UNSUPPORTED, + "Error decoding SFTP Packet", + ) + .await?; + } WireError::RanOut => match Self::handle_ran_out( &mut self.file_server, output_producer, @@ -413,6 +432,36 @@ where } }; } + WireError::UnknownPacket { number } => { + warn!( + "Unknown packet: packetId = {:?}. Will flush \ + its length and send unsupported back", + number + ); + /* TODO: The packet is unknown, but we are not consuming it properly. + That has the side effect that we will be interpreting chunks of that packet + as new packets and sending more garbage back to the client + Will result on an Close and not simply a clean message saying that + - Peek out the ReqId + - Peek out the length + + - flush the packet len if possible (do we have the whole length?) + + - Send an StatusCode with the relevant ReqId + + - Move on! + */ + let req_id = ReqId(source.peak_packet_req_id()?); + let len = source.peak_total_packet_len()? as usize; + source.consume_first(len)?; + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_OP_UNSUPPORTED, + "Error decoding SFTP Packet", + ) + .await?; + } _ => { error!("Error decoding SFTP Packet: {:?}", e); output_producer From c735f6ab5a7589a1a3cc97c9821f7e887cc0a1a0 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 10:11:49 +1100 Subject: [PATCH 165/393] One more time I missed the Cargo.lock changes --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 500e36e1..0176d4c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2851,7 +2851,7 @@ dependencies = [ [[package]] name = "sunset-demo-sftp-std" -version = "0.1.0" +version = "0.1.2" dependencies = [ "async-io", "critical-section", @@ -2915,7 +2915,7 @@ dependencies = [ [[package]] name = "sunset-sftp" -version = "0.1.1" +version = "0.1.2" dependencies = [ "embassy-futures", "embassy-sync 0.7.2", From bca0a67fbf0d5b0bde25bc55e08052049e7c577d Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 10:13:11 +1100 Subject: [PATCH 166/393] Adding test stats: For now I get an expected `Operation unsupported` --- demo/sftp/std/testing/test_stats.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100755 demo/sftp/std/testing/test_stats.sh diff --git a/demo/sftp/std/testing/test_stats.sh b/demo/sftp/std/testing/test_stats.sh new file mode 100755 index 00000000..f079fe3d --- /dev/null +++ b/demo/sftp/std/testing/test_stats.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +echo "Testing Stats..." + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Define test files +FILES=("512B_random") + +# Generate random data files +echo "Generating random data files..." +dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null + +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." + +# Upload all files +sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +$(printf 'put ./%s\n' "${FILES[@]}") +$(printf 'ls -lh ./%s\n' "${FILES[@]}") + +bye +EOF + +echo "Cleaning up local files..." +rm -f -r ./*_random ./out/*_random From f5de051e8e07e6ef20a3b5d65035c81ca43486ad Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 11:44:05 +1100 Subject: [PATCH 167/393] refactored std helper to expose get_file_attrs --- demo/sftp/std/src/demosftpserver.rs | 15 +++++++++++++++ sftp/src/lib.rs | 1 + 2 files changed, 16 insertions(+) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 994626ed..72193b82 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -323,4 +323,19 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { return Err(StatusCode::SSH_FX_NO_SUCH_FILE); } } + + fn liststats(&mut self, file_path: &str) -> SftpOpResult { + log::debug!("SftpServer ListStats: file_path = {:?}", file_path); + let file_path = Path::new(file_path); + if file_path.is_file() { + return Ok(sunset_sftp::server::helpers::get_file_attrs( + file_path.metadata().map_err(|err| { + error!("Problem listing stats: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?, + )); + } else { + return Err(StatusCode::SSH_FX_NO_SUCH_FILE); + } + } } diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 6f518fd1..06b56d4a 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -84,6 +84,7 @@ pub mod server { #[cfg(feature = "std")] pub use crate::sftpserver::DirEntriesCollection; + pub use crate::sftpserver::get_file_attrs; } pub use crate::sftpsink::SftpSink; pub use sunset::sshwire::SSHEncode; From cb56c6b88b7b21f9a82729bcc047abcc61c2be9a Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 11:45:12 +1100 Subject: [PATCH 168/393] completed getting file stats Tested with `test_get.sh` Ticked off from the roadmap --- demo/sftp/std/testing/test_get.sh | 27 ++++++++ sftp/src/lib.rs | 2 +- sftp/src/proto.rs | 10 +++ sftp/src/sftphandler/sftphandler.rs | 21 ++++++- sftp/src/sftpserver.rs | 95 ++++++++++++++++------------- 5 files changed, 111 insertions(+), 44 deletions(-) create mode 100755 demo/sftp/std/testing/test_get.sh diff --git a/demo/sftp/std/testing/test_get.sh b/demo/sftp/std/testing/test_get.sh new file mode 100755 index 00000000..e191895f --- /dev/null +++ b/demo/sftp/std/testing/test_get.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +echo "Testing Stats..." + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Define test files +FILES=("512B_random") + +# Generate random data files +echo "Generating random data files..." +dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null + +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." + +# Upload all files +sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +$(printf 'put ./%s\n' "${FILES[@]}") +$(printf 'get ./%s\n' "${FILES[@]}") + +bye +EOF + +echo "Cleaning up local files..." +rm -f -r ./*_random ./out/*_random diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 06b56d4a..9050cbb1 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -27,7 +27,7 @@ //! and [write](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) //! - [x] Directory [Browsing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7) //! - [ ] File [read](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4), -//! - [ ] File [stats](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8) +//! - [x] File [stats](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8) //! //! ## Minimal features for convenient usability //! diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 9dd5fa17..6fab4e74 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -158,6 +158,14 @@ pub struct Write<'a> { pub data: BinString<'a>, } +/// Used for `ssh_fxp_lstat` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8). +/// LSTAT does not follow symbolic links +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct LStat<'a> { + /// The path of the element which stats are to be retrieved + pub file_path: TextString<'a>, +} + // ============================= Responses ============================= /// Used for `ssh_fxp_realpath` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.11). @@ -848,6 +856,7 @@ sftpmessages! [ (4, Close, Close<'a>, "ssh_fxp_close"), (5, Read, Read<'a>, "ssh_fxp_read"), (6, Write, Write<'a>, "ssh_fxp_write"), + (7, LStat, LStat<'a>, "ssh_fxp_lstat"), (11, OpenDir, OpenDir<'a>, "ssh_fxp_opendir"), (12, ReadDir, ReadDir<'a>, "ssh_fxp_readdir"), (16, PathInfo, PathInfo<'a>, "ssh_fxp_realpath"), @@ -858,6 +867,7 @@ sftpmessages! [ (102, Handle, Handle<'a>, "ssh_fxp_handle"), (103, Data, Data<'a>, "ssh_fxp_data"), (104, Name, Name, "ssh_fxp_name"), + (105, Attrs, Attrs, "ssh_fxp_attrs"), }, ]; diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index fc94392d..beb17507 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -3,8 +3,8 @@ use super::PartialWriteRequestTracker; use crate::error::SftpError; use crate::handles::OpaqueFileHandle; use crate::proto::{ - self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, - SftpPacket, StatusCode, + self, InitVersionLowest, LStat, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, + SftpNum, SftpPacket, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; use crate::server::DirReply; @@ -659,6 +659,23 @@ where .await?; }; } + SftpPacket::LStat(req_id, LStat { file_path: path }) => { + match file_server.liststats(path.as_str()?) { + Ok(attrs) => { + debug!("List stats for {} is {:?}", path, attrs); + + output_producer + .send_packet(&SftpPacket::Attrs(req_id, attrs)) + .await?; + } + Err(status) => { + error!("Error listing stats for {}: {:?}", path, status); + output_producer + .send_status(req_id, status, "Could not list attributes") + .await?; + } + } + } _ => { error!("Unsupported request type: {:?}", request); return Err(SftpError::NotSupported); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 5204c20d..2cc047b4 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -120,7 +120,6 @@ where /// to the client since the client will only stop asking for more elements in the /// directory when a read dir request is answer with an reply.send_eof() /// - #[allow(unused_variables)] async fn readdir( &mut self, @@ -139,6 +138,15 @@ where log::error!("SftpServer RealPath operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } + + /// Provides the stats of the given file path. It does not follow links + fn liststats(&mut self, file_path: &str) -> SftpOpResult { + log::error!( + "SftpServer ListStats operation not defined: file_path = {:?}", + file_path + ); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } } // TODO Define this @@ -167,25 +175,6 @@ pub struct ChanOut<'g, 'a> { _phantom_a: PhantomData<&'a ()>, // a' Why the second lifetime if ChanIO only needs one } -// TODO: complete this once the flow is fully developed -// `/ The usage is simple: -// / -// / 1. SftpHandler will: Initialize the structure -// / 2. The `SftpServer` trait implementation for 'readdir()' will: -// / -// / - Receive the DirReply ref 'reply' -// / -// / a. If there are items to send: -// / -// / - Instantiate a [`DirEntriesCollection`] with the items in the requested folder -// / - call the 'DirEntriesCollection.SendHeader(reply)' -// / - call the 'DirEntriesCollection.send_entries(reply)' -// / -// / b. If there are no items to send: -// / -// / - Call the 'reply.send_eof()' -// TODO Define this - /// Uses for [`DirReply`] to: /// /// - In case of no more items in the directory to be sent, call `reply.send_eof()` @@ -408,28 +397,52 @@ impl DirEntriesCollection { fn get_attrs_or_empty( maybe_metadata: Result, ) -> Attrs { - maybe_metadata.map(Self::get_attrs).unwrap_or_default() + maybe_metadata.map(get_file_attrs).unwrap_or_default() } - fn get_attrs(metadata: Metadata) -> Attrs { - let time_to_u32 = |time_result: std::io::Result| { - time_result - .ok()? - .duration_since(SystemTime::UNIX_EPOCH) - .ok()? - .as_secs() - .try_into() - .ok() - }; - - Attrs { - size: Some(metadata.len()), - uid: Some(metadata.st_uid()), - gid: Some(metadata.st_gid()), - permissions: Some(metadata.permissions().mode()), - atime: time_to_u32(metadata.accessed()), - mtime: time_to_u32(metadata.modified()), - ext_count: None, - } + // fn get_attrs(metadata: Metadata) -> Attrs { + // let time_to_u32 = |time_result: std::io::Result| { + // time_result + // .ok()? + // .duration_since(SystemTime::UNIX_EPOCH) + // .ok()? + // .as_secs() + // .try_into() + // .ok() + // }; + + // Attrs { + // size: Some(metadata.len()), + // uid: Some(metadata.st_uid()), + // gid: Some(metadata.st_gid()), + // permissions: Some(metadata.permissions().mode()), + // atime: time_to_u32(metadata.accessed()), + // mtime: time_to_u32(metadata.modified()), + // ext_count: None, + // } + // } +} + +#[cfg(feature = "std")] +/// [`std`] helper function to get [`Attrs`] from a [`Metadata`]. +pub fn get_file_attrs(metadata: Metadata) -> Attrs { + let time_to_u32 = |time_result: std::io::Result| { + time_result + .ok()? + .duration_since(SystemTime::UNIX_EPOCH) + .ok()? + .as_secs() + .try_into() + .ok() + }; + + Attrs { + size: Some(metadata.len()), + uid: Some(metadata.st_uid()), + gid: Some(metadata.st_gid()), + permissions: Some(metadata.permissions().mode()), + atime: time_to_u32(metadata.accessed()), + mtime: time_to_u32(metadata.modified()), + ext_count: None, } } From 9bc53c327f0fd26cd02954c9e5accd168ff316fd Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 16:58:16 +1100 Subject: [PATCH 169/393] [skip ci] Fixed misrepresentation of SSH_FXP_OPEN packet also fixing an error with no_std for std helper function Removed doc orphan document comments --- sftp/src/lib.rs | 10 ++-- sftp/src/proto.rs | 143 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 6 deletions(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 9050cbb1..4e514b6d 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -60,10 +60,10 @@ mod sftpserver; mod sftpsink; mod sftpsource; -/// Main calling point for the library provided that the user implements -/// a [`crate::sftpserver::SftpServer`]. -/// -/// Please see basic usage at `../demo/sftd/std` +// Main calling point for the library provided that the user implements +// a [`server::SftpServer`]. +// +// Please see basic usage at `../demo/sftd/std` pub use sftphandler::SftpHandler; /// Structures and types used to add the details for the target system @@ -84,6 +84,7 @@ pub mod server { #[cfg(feature = "std")] pub use crate::sftpserver::DirEntriesCollection; + #[cfg(feature = "std")] pub use crate::sftpserver::get_file_attrs; } pub use crate::sftpsink::SftpSink; @@ -104,6 +105,7 @@ pub mod protocol { pub use crate::proto::Filename; pub use crate::proto::Name; pub use crate::proto::NameEntry; + pub use crate::proto::PFlags; pub use crate::proto::PathInfo; pub use crate::proto::StatusCode; /// Constants that might be useful for SFTP developers diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 6fab4e74..8c7b6654 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -106,11 +106,66 @@ pub struct Open<'a> { /// The relative or absolute path of the file to be open pub filename: Filename<'a>, /// File [permissions flags](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3) - pub pflags: u32, + pub pflags: PFlags, /// Initial attributes for the file pub attrs: Attrs, } +/// Flags for Open RequestFor more information see [Opening, creating and closing files](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3) +/// TODO: Reference! This is packed as u32 since that is the field data type in specs +#[derive(Debug, FromPrimitive, PartialEq)] +#[repr(u32)] +#[allow(non_camel_case_types, missing_docs)] +pub enum PFlags { + //#[sshwire(variant = "ssh_fx_read")] + SSH_FXF_READ = 0x00000001, + //#[sshwire(variant = "ssh_fx_write")] + SSH_FXF_WRITE = 0x00000002, + //#[sshwire(variant = "ssh_fx_append")] + SSH_FXF_APPEND = 0x00000004, + //#[sshwire(variant = "ssh_fx_creat")] + SSH_FXF_CREAT = 0x00000008, + //#[sshwire(variant = "ssh_fx_trunk")] + SSH_FXF_TRUNC = 0x00000010, + //#[sshwire(variant = "ssh_fx_excl")] + SSH_FXF_EXCL = 0x00000020, + //#[sshwire(unknown)] + #[num_enum(catch_all)] + Multiple(u32), +} + +impl<'de> SSHDecode<'de> for PFlags { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + Ok(PFlags::from(u32::dec(s)?)) + } +} + +// TODO: Implement an automatic from implementation for u32 to Status code +// This is prone to errors if we update PFlags enum +impl From<&PFlags> for u32 { + fn from(value: &PFlags) -> Self { + match value { + PFlags::SSH_FXF_READ => 0x00000001, + PFlags::SSH_FXF_WRITE => 0x00000002, + PFlags::SSH_FXF_APPEND => 0x00000004, + PFlags::SSH_FXF_CREAT => 0x00000008, + PFlags::SSH_FXF_TRUNC => 0x00000010, + PFlags::SSH_FXF_EXCL => 0x00000020, + PFlags::Multiple(value) => *value, + } + } +} +// TODO: Implement an SSHEncode attribute for enums to encode them in a given numeric format +impl SSHEncode for PFlags { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + let numeric_value: u32 = self.into(); + numeric_value.enc(s) + } +} + /// Used for `ssh_fxp_open` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7). #[derive(Debug, SSHEncode, SSHDecode)] pub struct OpenDir<'a> { @@ -166,6 +221,14 @@ pub struct LStat<'a> { pub file_path: TextString<'a>, } +/// Used for `ssh_fxp_lstat` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8). +/// STAT does follow symbolic links +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct Stat<'a> { + /// The path of the element which stats are to be retrieved + pub file_path: TextString<'a>, +} + // ============================= Responses ============================= /// Used for `ssh_fxp_realpath` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.11). @@ -367,7 +430,7 @@ pub struct ExtPair<'a> { /// See [File Attributes](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-5) /// for more information. #[allow(missing_docs)] -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct Attrs { pub size: Option, pub uid: Option, @@ -860,6 +923,7 @@ sftpmessages! [ (11, OpenDir, OpenDir<'a>, "ssh_fxp_opendir"), (12, ReadDir, ReadDir<'a>, "ssh_fxp_readdir"), (16, PathInfo, PathInfo<'a>, "ssh_fxp_realpath"), + (17, Stat, Stat<'a>, "ssh_fxp_stat"), }, response: { @@ -876,6 +940,14 @@ mod proto_tests { use super::*; use crate::server::SftpSink; + // TODO: Create tests for every SftpPacket. A good starting point is a + // roadtrip test + + #[cfg(test)] + extern crate std; + #[cfg(test)] + use std::println; + #[test] fn test_status_encoding() { let mut buf = [0u8; 256]; @@ -905,4 +977,71 @@ mod proto_tests { assert_eq!(&expected_status_packet_slice, sink.used_slice()); } + + #[test] + fn test_attributes_roundtrip() { + let mut buff = [0u8; MAX_NAME_ENTRY_SIZE]; + let attr_read_only = Attrs { + size: Some(1), + uid: Some(2), + gid: Some(3), + permissions: Some(222), + atime: Some(4), + mtime: Some(5), + ext_count: None, + // ext_count: Some(10), // TODO: This does not get deserialized + }; + + let mut sink = SftpSink::new(&mut buff); + attr_read_only.enc(&mut sink).unwrap(); + println!( + "attr_read_only encoded_len = {:?}, encoded = {:?}", + sink.payload_len(), + sink.payload_slice() + ); + let mut source = SftpSource::new(sink.payload_slice()); + println!("source = {:?}", source); + + let a_r = Attrs::dec(&mut source); + match a_r { + Ok(attrs) => { + println!("source = {:?}", attrs); + assert_eq!(attr_read_only, attrs); + } + Err(e) => panic!("The attributes could not be decoded: {:?}", e), + } + } + + #[test] + fn test_packet_open_reading() { + let buff_open_read = [ + 0u8, 0, 0, + 58, // Len + 3, // SftpPacket + 0, 0, 0, + 4, // ReqId + 0, 0, 0, + 41, // Text String len + 46, 47, 100, 101, 109, 111, 47, 115, 102, + 116, // file Path + 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, + 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, + 109, // and 41 + 0, 0, 0, + 1, // PFlags: 1u32 == SSSH_FXF_READ + 0, 0, 0, + 0, // Attrib flags == 0 No flags, no attributes + ]; + + let mut source = SftpSource::new(&buff_open_read); + println!("source = {:?}", source); + + match SftpPacket::decode_request(&mut source) { + Ok(SftpPacket::Open(req_id, open)) => { + assert_eq!(PFlags::SSH_FXF_READ, open.pflags); + } + Ok(other) => panic!("Expected Open packet, got: {:?}", other), + Err(e) => panic!("Failed to decode packet: {:?}", e), + } + } } From 1d904ad46ad11a4a9396fb1461e3a3ee7d1cefcd Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 17:04:17 +1100 Subject: [PATCH 170/393] Fixing SftpTrait and implementations to use the proper Open mode (PFlags) Tested with `test_get.sh` Next step: Implement SSH_FXP_READ --- demo/sftp/std/src/demosftpserver.rs | 52 ++++++++++++++++++----------- demo/sftp/std/testing/test_get.sh | 35 +++++++++++++++++-- sftp/src/sftphandler/sftphandler.rs | 23 +++++++++++-- sftp/src/sftpserver.rs | 36 +++++--------------- 4 files changed, 94 insertions(+), 52 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 72193b82..2fbbfc01 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -4,7 +4,7 @@ use crate::{ }; use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; -use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; +use sunset_sftp::protocol::{Attrs, Filename, NameEntry, PFlags, StatusCode}; use sunset_sftp::server::helpers::DirEntriesCollection; use sunset_sftp::server::{ DirReply, ReadReply, ReadStatus, SftpOpResult, SftpServer, @@ -15,6 +15,8 @@ use log::{debug, error, info, log, trace, warn}; use std::fs; use std::{fs::File, os::unix::fs::FileExt, path::Path}; +use std::os::unix::fs::PermissionsExt; + #[derive(Debug)] pub(crate) enum PrivatePathHandle { File(PrivateFileHandle), @@ -105,16 +107,13 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { fn open( &mut self, filename: &str, - attrs: &Attrs, + mode: &PFlags, ) -> SftpOpResult { - debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); - - let poxit_attr = attrs - .permissions - .as_ref() - .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED)?; - let can_write = poxit_attr & 0o222 > 0; - let can_read = poxit_attr & 0o444 > 0; + debug!("Open file: filename = {:?}, mode = {:?}", filename, mode); + + let can_write = u32::from(mode) & u32::from(&PFlags::SSH_FXF_WRITE) > 0; + let can_read = u32::from(mode) & u32::from(&PFlags::SSH_FXF_READ) > 0; + debug!( "File open for read/write access: can_read={:?}, can_write={:?}", can_read, can_write @@ -123,14 +122,21 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { let file = File::options() .read(can_read) .write(can_write) - .create(true) + .create(can_write) .open(filename) .map_err(|_| StatusCode::SSH_FX_FAILURE)?; + let permissions = file + .metadata() + .map_err(|_| StatusCode::SSH_FX_FAILURE)? + .permissions() + .mode() + & 0o777; + let fh = self.handles_manager.insert( PrivatePathHandle::File(PrivateFileHandle { path: filename.into(), - permissions: attrs.permissions, + permissions: Some(permissions), file, }), OPAQUE_SALT, @@ -324,16 +330,24 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } } - fn liststats(&mut self, file_path: &str) -> SftpOpResult { + fn stats(&mut self, follow_links: bool, file_path: &str) -> SftpOpResult { log::debug!("SftpServer ListStats: file_path = {:?}", file_path); let file_path = Path::new(file_path); + + let metadata = if follow_links { + file_path.metadata() // follows symlinks + } else { + file_path.symlink_metadata() // doesn't follow symlinks + } + .map_err(|err| { + error!("Problem listing stats: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; + if file_path.is_file() { - return Ok(sunset_sftp::server::helpers::get_file_attrs( - file_path.metadata().map_err(|err| { - error!("Problem listing stats: {:?}", err); - StatusCode::SSH_FX_FAILURE - })?, - )); + return Ok(sunset_sftp::server::helpers::get_file_attrs(metadata)); + } else if file_path.is_symlink() { + return Ok(sunset_sftp::server::helpers::get_file_attrs(metadata)); } else { return Err(StatusCode::SSH_FX_NO_SUCH_FILE); } diff --git a/demo/sftp/std/testing/test_get.sh b/demo/sftp/std/testing/test_get.sh index e191895f..d4f007b6 100755 --- a/demo/sftp/std/testing/test_get.sh +++ b/demo/sftp/std/testing/test_get.sh @@ -15,13 +15,44 @@ dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." -# Upload all files +# Upload the files sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'put ./%s\n' "${FILES[@]}") -$(printf 'get ./%s\n' "${FILES[@]}") bye EOF +echo "UPLOAD Test Results:" +echo "=============" +# Test each file +for file in "${FILES[@]}"; do + if diff "./${file}" "./out/${file}" >/dev/null 2>&1; then + echo "Upload PASS: ${file}" + else + echo "Upload FAIL: ${file}" + fi +done + +echo "Cleaning up original files..." +rm -f -r ./*_random + +# Download the files +sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +$(printf 'get ./%s\n' "${FILES[@]}") +bye +EOF + +echo "DOWNLOAD Test Results:" +echo "=============" +# Test each file +for file in "${FILES[@]}"; do + if diff "./${file}" "./out/${file}" >/dev/null 2>&1; then + echo "Download PASS: ${file}" + else + echo "Download FAIL: ${file}" + fi +done + + echo "Cleaning up local files..." rm -f -r ./*_random ./out/*_random diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index beb17507..9716b4bd 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -4,7 +4,7 @@ use crate::error::SftpError; use crate::handles::OpaqueFileHandle; use crate::proto::{ self, InitVersionLowest, LStat, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, - SftpNum, SftpPacket, StatusCode, + SftpNum, SftpPacket, Stat, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; use crate::server::DirReply; @@ -563,7 +563,7 @@ where dir_reply.send_item(&name_entry).await?; } SftpPacket::Open(req_id, open) => { - match file_server.open(open.filename.as_str()?, &open.attrs) { + match file_server.open(open.filename.as_str()?, &open.pflags) { Ok(opaque_file_handle) => { let response = SftpPacket::Handle( req_id, @@ -660,7 +660,24 @@ where }; } SftpPacket::LStat(req_id, LStat { file_path: path }) => { - match file_server.liststats(path.as_str()?) { + match file_server.stats(false, path.as_str()?) { + Ok(attrs) => { + debug!("List stats for {} is {:?}", path, attrs); + + output_producer + .send_packet(&SftpPacket::Attrs(req_id, attrs)) + .await?; + } + Err(status) => { + error!("Error listing stats for {}: {:?}", path, status); + output_producer + .send_status(req_id, status, "Could not list attributes") + .await?; + } + } + } + SftpPacket::Stat(req_id, Stat { file_path: path }) => { + match file_server.stats(true, path.as_str()?) { Ok(attrs) => { debug!("List stats for {} is {:?}", path, attrs); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 2cc047b4..5c954443 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,6 +1,6 @@ use crate::error::SftpResult; use crate::proto::{ - ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, MAX_NAME_ENTRY_SIZE, NameEntry, + ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, MAX_NAME_ENTRY_SIZE, NameEntry, PFlags, }; use crate::server::SftpSink; use crate::sftphandler::SftpOutputProducer; @@ -51,11 +51,11 @@ where T: OpaqueFileHandle, { /// Opens a file for reading/writing - fn open(&'_ mut self, path: &str, attrs: &Attrs) -> SftpOpResult { + fn open(&'_ mut self, path: &str, mode: &PFlags) -> SftpOpResult { log::error!( "SftpServer Open operation not defined: path = {:?}, attrs = {:?}", path, - attrs + mode ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } @@ -139,10 +139,12 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - /// Provides the stats of the given file path. It does not follow links - fn liststats(&mut self, file_path: &str) -> SftpOpResult { + /// Provides the stats of the given file path + fn stats(&mut self, follow_links: bool, file_path: &str) -> SftpOpResult { log::error!( - "SftpServer ListStats operation not defined: file_path = {:?}", + "SftpServer Stats operation not defined: follow_link = {:?}, \ + file_path = {:?}", + follow_links, file_path ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -399,28 +401,6 @@ impl DirEntriesCollection { ) -> Attrs { maybe_metadata.map(get_file_attrs).unwrap_or_default() } - - // fn get_attrs(metadata: Metadata) -> Attrs { - // let time_to_u32 = |time_result: std::io::Result| { - // time_result - // .ok()? - // .duration_since(SystemTime::UNIX_EPOCH) - // .ok()? - // .as_secs() - // .try_into() - // .ok() - // }; - - // Attrs { - // size: Some(metadata.len()), - // uid: Some(metadata.st_uid()), - // gid: Some(metadata.st_gid()), - // permissions: Some(metadata.permissions().mode()), - // atime: time_to_u32(metadata.accessed()), - // mtime: time_to_u32(metadata.modified()), - // ext_count: None, - // } - // } } #[cfg(feature = "std")] From 7cce6fb4156b5e0165c336098cf0a817f13bb2b9 Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Mon, 17 Nov 2025 20:13:35 +1100 Subject: [PATCH 171/393] Address event naming conventions, remove wants_reply field even if listed in https://datatracker.ietf.org/doc/html/rfc4254#section-6.4 --- demo/common/src/server.rs | 2 +- src/event.rs | 6 +++--- src/packets.rs | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/demo/common/src/server.rs b/demo/common/src/server.rs index 2207fef7..b5c98a1d 100644 --- a/demo/common/src/server.rs +++ b/demo/common/src/server.rs @@ -118,7 +118,7 @@ impl DemoCommon { ServEvent::PubkeyAuth(a) => self.handle_pubkey(a), ServEvent::OpenSession(a) => self.open_session(a), ServEvent::SessionPty(a) => a.succeed(), - ServEvent::Environment(a) => a.succeed(), + ServEvent::SessionEnv(a) => a.succeed(), ServEvent::SessionSubsystem(a) => { info!("Ignored request for subsystem '{}'", a.command()?); Ok(()) diff --git a/src/event.rs b/src/event.rs index 106825e0..2c422982 100644 --- a/src/event.rs +++ b/src/event.rs @@ -301,7 +301,7 @@ pub enum ServEvent<'g, 'a> { /// TODO details SessionPty(ServPtyRequest<'g, 'a>), /// Server has received one environment variable - Environment(ServEnvironmentRequest<'g, 'a>), + SessionEnv(ServEnvironmentRequest<'g, 'a>), /// The SSH session is no longer running #[allow(unused)] @@ -327,7 +327,7 @@ impl Debug for ServEvent<'_, '_> { Self::SessionExec(_) => "SessionExec", Self::SessionSubsystem(_) => "SessionSubsystem", Self::SessionPty(_) => "SessionPty", - Self::Environment(_) => "Environment", + Self::SessionEnv(_) => "Environment", Self::Defunct => "Defunct", Self::PollAgain => "PollAgain", }; @@ -872,7 +872,7 @@ impl ServEventId { } Self::Environment { num } => { debug_assert!(matches!(p, Some(Packet::ChannelRequest(_)))); - Ok(ServEvent::Environment(ServEnvironmentRequest::new(runner, num))) + Ok(ServEvent::SessionEnv(ServEnvironmentRequest::new(runner, num))) } Self::Defunct => Ok(ServEvent::Defunct), } diff --git a/src/packets.rs b/src/packets.rs index 8026a035..edec58ae 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -771,7 +771,6 @@ pub struct WinChange { #[derive(Debug, SSHEncode, SSHDecode)] #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] pub struct Environment<'a> { - // pub wants_reply: bool, // TODO: needed? pub name: TextString<'a>, pub value: TextString<'a>, } From 89488611b2032d3dfbfc9858414528c16829555d Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Mon, 17 Nov 2025 20:21:26 +1100 Subject: [PATCH 172/393] Add small comment on input string sanitisation state to warn C/FFI bindings --- src/event.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/event.rs b/src/event.rs index 2c422982..5e21d413 100644 --- a/src/event.rs +++ b/src/event.rs @@ -300,7 +300,8 @@ pub enum ServEvent<'g, 'a> { /// /// TODO details SessionPty(ServPtyRequest<'g, 'a>), - /// Server has received one environment variable + /// Server has received one environment variable. + /// Note: input strings are not sanitised. SessionEnv(ServEnvironmentRequest<'g, 'a>), /// The SSH session is no longer running From 00657dc94146620841df7ec86dcdf1fad6ab6738 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 17 Nov 2025 19:31:07 +0800 Subject: [PATCH 173/393] Use #[expect] rather than allow --- src/conn.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conn.rs b/src/conn.rs index b6f03329..074cd993 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -210,7 +210,7 @@ impl CliServ for server::Server { Some(self) } - #[allow(private_interfaces)] + #[expect(private_interfaces)] fn dispatch_into_event<'a, 'g>( runner: &'g mut Runner<'a, Self>, disp: DispatchEvent, From 64b26c1e340f51ed72dee8181da338311f2fe277 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 17 Nov 2025 22:22:42 +0800 Subject: [PATCH 174/393] Bump minimum version to 1.87 is_multiple_of() requires 1.87 --- .github/workflows/ci.yml | 4 ++-- Cargo.toml | 1 + README.md | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a23e60f..b5be8aeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,8 @@ jobs: all: strategy: matrix: - # 1.85 is an arbitrary minimum, tested to notice when it bumps - rust_version: [stable, nightly, 1.85] + # 1.87 is an arbitrary minimum, tested to notice when it bumps + rust_version: [stable, nightly, 1.87] runs-on: ubuntu-latest env: RUSTUP_TOOLCHAIN: ${{ matrix.rust_version }} diff --git a/Cargo.toml b/Cargo.toml index 69ff1cec..df2cb287 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ repository = "https://github.com/mkj/sunset" categories = ["network-programming", "embedded", "no-std"] license = "0BSD" keywords = ["ssh"] +rust-version = "1.87" [workspace] members = [ diff --git a/README.md b/README.md index 918beb09..b92559d7 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Desirable: ## Rust versions -At the time of writing Sunset will build with Rust 1.83. +At the time of writing Sunset will build with Rust 1.87. The requirement may increase whenever useful, targetting stable. ## Checks From 43faacb5b91ab3dffdc5e3dbb55006deba70d62a Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 17 Nov 2025 20:30:40 +0800 Subject: [PATCH 175/393] Fix warnings reported by clippy Mostly with --fix, some implemented manually --- async/src/client.rs | 2 +- demo/common/src/config.rs | 11 ++++--- demo/common/src/menu.rs | 4 +-- demo/common/src/menu_buf.rs | 2 +- demo/common/src/server.rs | 2 +- demo/picow/src/flashconfig.rs | 12 ++++---- demo/picow/src/picowmenu.rs | 6 ++-- demo/picow/src/usb.rs | 10 +++---- demo/picow/src/wifi.rs | 2 +- demo/std/src/main.rs | 2 +- demo/std/src/setupmenu.rs | 4 +-- src/channel.rs | 54 ++++++++++++++++++----------------- src/cliauth.rs | 6 ++-- src/conn.rs | 16 +++-------- src/encrypt.rs | 2 +- src/kex.rs | 12 ++++---- src/lib.rs | 3 +- src/runner.rs | 8 ++++-- src/servauth.rs | 2 +- src/sshwire.rs | 4 +-- src/traffic.rs | 15 ++++------ sshwire-derive/src/lib.rs | 1 + stdasync/src/knownhosts.rs | 24 +++++++--------- 23 files changed, 99 insertions(+), 105 deletions(-) diff --git a/async/src/client.rs b/async/src/client.rs index 87eb54ef..857a5bbe 100644 --- a/async/src/client.rs +++ b/async/src/client.rs @@ -48,7 +48,7 @@ impl<'a> SSHClient<'a> { ) -> Result> { match self.sunset.progress(ph).await? { Event::Cli(x) => Ok(x), - Event::None => return Ok(CliEvent::PollAgain), + Event::None => Ok(CliEvent::PollAgain), Event::Progressed => Ok(CliEvent::PollAgain), _ => Err(Error::bug()), } diff --git a/demo/common/src/config.rs b/demo/common/src/config.rs index a1df9780..bbcba331 100644 --- a/demo/common/src/config.rs +++ b/demo/common/src/config.rs @@ -99,7 +99,7 @@ impl SSHConfig { } pub fn set_console_pw(&mut self, pw: Option<&str>) -> Result<()> { - self.console_pw = pw.map(|p| PwHash::new(p)).transpose()?; + self.console_pw = pw.map(PwHash::new).transpose()?; Ok(()) } @@ -112,7 +112,7 @@ impl SSHConfig { } pub fn set_admin_pw(&mut self, pw: Option<&str>) -> Result<()> { - self.admin_pw = pw.map(|p| PwHash::new(p)).transpose()?; + self.admin_pw = pw.map(PwHash::new).transpose()?; Ok(()) } @@ -182,7 +182,7 @@ where return Err(WireError::PacketWrong); } let gw: Option = dec_option(s)?; - let gateway = gw.map(|gw| Ipv4Address::from_bits(gw)); + let gateway = gw.map(Ipv4Address::from_bits); Ok(StaticConfigV4 { address: Ipv4Cidr::new(ad, prefix), gateway, @@ -308,14 +308,13 @@ impl PwHash { return false; } let prehash = Self::prehash(pw, &self.salt); - let check_hash = - bcrypt::bcrypt(self.cost as u32, self.salt.clone(), &prehash); + let check_hash = bcrypt::bcrypt(self.cost as u32, self.salt, &prehash); check_hash.ct_eq(&self.hash).into() } fn prehash(pw: &str, salt: &[u8]) -> [u8; 32] { // OK unwrap: can't fail, accepts any length - let mut prehash = Hmac::::new_from_slice(&salt).unwrap(); + let mut prehash = Hmac::::new_from_slice(salt).unwrap(); prehash.update(pw.as_bytes()); prehash.finalize().into_bytes().into() } diff --git a/demo/common/src/menu.rs b/demo/common/src/menu.rs index 8de176d1..ed617340 100644 --- a/demo/common/src/menu.rs +++ b/demo/common/src/menu.rs @@ -391,7 +391,7 @@ where Some(arg) => { match menu.items.iter().find(|i| i.command == arg) { Some(item) => { - self.print_long_help(&item); + self.print_long_help(item); } None => { writeln!( @@ -406,7 +406,7 @@ where _ => { writeln!(self.context, "AVAILABLE ITEMS:").unwrap(); for item in menu.items { - self.print_short_help(&item); + self.print_short_help(item); } if self.depth != 0 { self.print_short_help(&Item { diff --git a/demo/common/src/menu_buf.rs b/demo/common/src/menu_buf.rs index 2176c736..96297665 100644 --- a/demo/common/src/menu_buf.rs +++ b/demo/common/src/menu_buf.rs @@ -17,7 +17,7 @@ impl AsyncMenuBuf { W: embedded_io_async::Write, { let mut b = self.s.as_str().as_bytes(); - while b.len() > 0 { + while !b.is_empty() { let l = w.write(b).await?; b = &b[l..]; } diff --git a/demo/common/src/server.rs b/demo/common/src/server.rs index 09e62d3f..8ca98157 100644 --- a/demo/common/src/server.rs +++ b/demo/common/src/server.rs @@ -42,7 +42,7 @@ pub async fn listen( continue; } - let r = session(&mut socket, &config, demo).await; + let r = session(&mut socket, config, demo).await; if let Err(e) = r { warn!("Ended with error {e:#?}"); } diff --git a/demo/picow/src/flashconfig.rs b/demo/picow/src/flashconfig.rs index fde94590..ebf93b31 100644 --- a/demo/picow/src/flashconfig.rs +++ b/demo/picow/src/flashconfig.rs @@ -48,8 +48,10 @@ struct FlashConfig<'a> { impl FlashConfig<'_> { const BUF_SIZE: usize = 4 + SSHConfig::BUF_SIZE + 32; } -const _: () = - assert!(FlashConfig::BUF_SIZE % 4 == 0, "flash reads must be a multiple of 4"); +const _: () = assert!( + FlashConfig::BUF_SIZE.is_multiple_of(4), + "flash reads must be a multiple of 4" +); fn config_hash(config: &SSHConfig) -> Result<[u8; 32]> { let mut h = sha2::Sha256::new(); @@ -113,8 +115,8 @@ pub async fn load(fl: &mut Fl<'_>) -> Result { pub async fn save(fl: &mut Fl<'_>, config: &SSHConfig) -> Result<()> { let sc = FlashConfig { version: SSHConfig::CURRENT_VERSION, - config: OwnOrBorrow::Borrow(&config), - hash: config_hash(&config)?, + config: OwnOrBorrow::Borrow(config), + hash: config_hash(config)?, }; let l = sshwire::write_ssh(&mut fl.buf, &sc)?; let buf = &fl.buf[..l]; @@ -133,7 +135,7 @@ pub async fn save(fl: &mut Fl<'_>, config: &SSHConfig) -> Result<()> { trace!("flash write"); fl.flash - .write(CONFIG_OFFSET, &buf) + .write(CONFIG_OFFSET, buf) .await .map_err(|_| Error::msg("flash write error"))?; diff --git a/demo/picow/src/picowmenu.rs b/demo/picow/src/picowmenu.rs index a9cbddc5..7d2b5ad0 100644 --- a/demo/picow/src/picowmenu.rs +++ b/demo/picow/src/picowmenu.rs @@ -511,7 +511,7 @@ const SERIAL_ITEM: Item = Item { }; fn enter_auth(context: &mut MenuCtx) { - let _ = writeln!(context, "In auth menu").unwrap(); + writeln!(context, "In auth menu").unwrap(); } fn endis(v: bool) -> &'static str { @@ -571,7 +571,7 @@ fn do_clear_key(_item: &Item, args: &[&str], context: &mut MenuCtx) { fn do_console_pw(_item: &Item, args: &[&str], context: &mut MenuCtx) { let pw = args[0]; - if pw.as_bytes().len() > MAX_PW_LEN { + if pw.len() > MAX_PW_LEN { let _ = writeln!(context, "Too long"); return; } @@ -612,7 +612,7 @@ fn do_console_clear_pw(_item: &Item, args: &[&str], context: &mut MenuC fn do_admin_pw(_item: &Item, args: &[&str], context: &mut MenuCtx) { let pw = args[0]; - if pw.as_bytes().len() > MAX_PW_LEN { + if pw.len() > MAX_PW_LEN { let _ = writeln!(context, "Too long"); return; } diff --git a/demo/picow/src/usb.rs b/demo/picow/src/usb.rs index f64e7248..724738eb 100644 --- a/demo/picow/src/usb.rs +++ b/demo/picow/src/usb.rs @@ -84,10 +84,10 @@ pub(crate) async fn task( let usb_fut = usb.run(); // console via SSH on if00 - let io0_run = console_if00_run(&global, cdc0); + let io0_run = console_if00_run(global, cdc0); // Admin menu on if02 - let io2_run = menu_if02_run(&global, cdc2); + let io2_run = menu_if02_run(global, cdc2); // keyboard // let hid_run = keyboard::run(&global, hid); @@ -192,9 +192,9 @@ impl<'a, D: Driver<'a>> Read for CDCRead<'a, '_, D> { let b = self.fill_buf().await?; let n = ret.len().min(b.len()); - (&mut ret[..n]).copy_from_slice(&b[..n]); + ret[..n].copy_from_slice(&b[..n]); self.consume(n); - return Ok(n); + Ok(n) } } @@ -214,7 +214,7 @@ impl<'a, D: Driver<'a>> BufRead for CDCRead<'a, '_, D> { } debug_assert!(self.end > 0); - return Ok(&self.buf[self.start..self.end]); + Ok(&self.buf[self.start..self.end]) } fn consume(&mut self, amt: usize) { diff --git a/demo/picow/src/wifi.rs b/demo/picow/src/wifi.rs index d961611a..4968a52b 100644 --- a/demo/picow/src/wifi.rs +++ b/demo/picow/src/wifi.rs @@ -64,7 +64,7 @@ pub(crate) async fn wifi_stack( ); static STATE: StaticCell = StaticCell::new(); - let state = STATE.init_with(|| cyw43::State::new()); + let state = STATE.init_with(cyw43::State::new); let (net_device, control, runner) = cyw43::new(state, pwr, spi, fw).await; spawner.spawn(wifi_task(runner)).unwrap(); diff --git a/demo/std/src/main.rs b/demo/std/src/main.rs index 8e7ef25a..a516b52a 100644 --- a/demo/std/src/main.rs +++ b/demo/std/src/main.rs @@ -173,7 +173,7 @@ async fn listen( stack: Stack<'static>, config: &'static SunsetMutex, ) -> ! { - let demo = StdDemo::default(); + let demo = StdDemo; demo_common::listen(stack, config, &demo).await } diff --git a/demo/std/src/setupmenu.rs b/demo/std/src/setupmenu.rs index 2da2faed..cfac3a45 100644 --- a/demo/std/src/setupmenu.rs +++ b/demo/std/src/setupmenu.rs @@ -96,11 +96,11 @@ const AUTH_ITEM: Item = Item { }; fn enter_top(context: &mut AsyncMenuBuf) { - let _ = writeln!(context, "In setup menu").unwrap(); + writeln!(context, "In setup menu").unwrap(); } fn enter_auth(context: &mut AsyncMenuBuf) { - let _ = writeln!(context, "In auth menu").unwrap(); + writeln!(context, "In auth menu").unwrap(); } fn do_auth_show( diff --git a/src/channel.rs b/src/channel.rs index bef70f9c..f8202fb0 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -73,7 +73,7 @@ impl Channels { let ch = self.get_any(num)?; match ch.state { - ChanState::InOpen | ChanState::Opening { .. } => { + ChanState::InOpen | ChanState::Opening => { error::BadChannel { num }.fail() } _ => Ok(ch), @@ -94,7 +94,7 @@ impl Channels { let ch = self.get_any_mut(num)?; match ch.state { - ChanState::InOpen | ChanState::Opening { .. } => { + ChanState::InOpen | ChanState::Opening => { error::BadChannel { num }.fail() } _ => Ok(ch), @@ -225,11 +225,11 @@ impl Channels { } pub(crate) fn have_recv_eof(&self, num: ChanNum) -> bool { - self.get(num).map_or(false, |c| c.have_recv_eof()) + self.get(num).is_ok_and(|c| c.have_recv_eof()) } pub(crate) fn is_closed(&self, num: ChanNum) -> bool { - self.get(num).map_or(false, |c| c.is_closed()) + self.get(num).is_ok_and(|c| c.is_closed()) } pub(crate) fn send_allowed(&self, num: ChanNum) -> Option { @@ -237,7 +237,7 @@ impl Channels { } pub(crate) fn valid_send(&self, num: ChanNum, dt: ChanData) -> bool { - self.get(num).map_or(false, |c| c.valid_send(dt)) + self.get(num).is_ok_and(|c| c.valid_send(dt)) } /// Wake the channel with a ready input data packet. @@ -467,18 +467,13 @@ impl Channels { // Discard the data, sunset can't handle this debug!("Ignoring unexpected dt data, code {}", p.code); ch.finished_input(p.data.0.len()); + } else if let Some(len) = NonZeroUsize::new(p.data.0.len()) { + // TODO check we are expecting input and dt is valid. + let di = + DataIn { num: ChanNum(p.num), dt: ChanData::Stderr, len }; + ev = DispatchEvent::Data(di); } else { - if let Some(len) = NonZeroUsize::new(p.data.0.len()) { - // TODO check we are expecting input and dt is valid. - let di = DataIn { - num: ChanNum(p.num), - dt: ChanData::Stderr, - len, - }; - ev = DispatchEvent::Data(di); - } else { - trace!("Zero length channeldataext"); - } + trace!("Zero length channeldataext"); } } Packet::ChannelEof(p) => { @@ -565,7 +560,7 @@ impl Channels { req: ChannelReqType::Subsystem(packets::Subsystem { subsystem: command }), .. - }) => Ok(command.clone()), + }) => Ok(*command), _ => Err(Error::bug()), } } @@ -575,7 +570,7 @@ impl Channels { Packet::ChannelRequest(ChannelRequest { req: ChannelReqType::Environment(packets::Environment { name, .. }), .. - }) => Ok(name.clone()), + }) => Ok(*name), _ => Err(Error::bug()), } } @@ -586,7 +581,7 @@ impl Channels { req: ChannelReqType::Environment(packets::Environment { name: _, value }), .. - }) => Ok(value.clone()), + }) => Ok(*value), _ => Err(Error::bug()), } } @@ -836,22 +831,30 @@ impl Channel { pub fn wake_read(&mut self, dt: ChanData, is_client: bool) { match dt { ChanData::Normal => { - self.read_waker.take().map(|w| w.wake()); + if let Some(w) = self.read_waker.take() { + w.wake() + } } ChanData::Stderr => { if is_client { - self.ext_waker.take().map(|w| w.wake()); + if let Some(w) = self.ext_waker.take() { + w.wake() + } } } } } pub fn wake_write(&mut self, dt: Option, is_client: bool) { - if dt == Some(ChanData::Normal) || dt == None { - self.read_waker.take().map(|w| w.wake()); + if dt == Some(ChanData::Normal) || dt.is_none() { + if let Some(w) = self.read_waker.take() { + w.wake() + } } - if !is_client && (dt == Some(ChanData::Normal) || dt == None) { - self.ext_waker.take().map(|w| w.wake()); + if !is_client && (dt == Some(ChanData::Normal) || dt.is_none()) { + if let Some(w) = self.ext_waker.take() { + w.wake() + } } } @@ -1157,7 +1160,6 @@ impl<'g, 'a> CliSessionOpener<'g, 'a> { /// /// This must be sent prior to requesting a shell or command. /// Shells using a PTY will only receive data on the stdin FD, not stderr. - // TODO: set a flag in the channel so that it drops data on stderr, to // avoid waiting forever for a consumer? pub fn pty(&mut self, pty: channel::Pty) -> Result<()> { diff --git a/src/cliauth.rs b/src/cliauth.rs index b3f99e64..28589677 100644 --- a/src/cliauth.rs +++ b/src/cliauth.rs @@ -117,7 +117,7 @@ impl CliAuth { key: &'b SignKey, sess_id: &'b SessId, ) -> Result> { - let p = req_packet_pubkey(&self.username, &key, None, true)?; + let p = req_packet_pubkey(&self.username, key, None, true)?; Ok(auth::AuthSigMsg::new(p, sess_id)) } @@ -145,7 +145,7 @@ impl CliAuth { // Sign the packet without the signature let msg = self.auth_sig_msg(key, sess_id)?; let sig = key.sign(&msg)?; - let p = req_packet_pubkey(&self.username, &key, Some(&sig), true)?; + let p = req_packet_pubkey(&self.username, key, Some(&sig), true)?; s.send(p)?; parse_ctx.cli_auth_type = None; @@ -169,7 +169,7 @@ impl CliAuth { return Ok(DispatchEvent::CliEvent(CliEventId::Pubkey)); }; - let p = req_packet_pubkey(&self.username, &key, Some(&sig), true)?; + let p = req_packet_pubkey(&self.username, key, Some(sig), true)?; s.send(p)?; Ok(DispatchEvent::None) } diff --git a/src/conn.rs b/src/conn.rs index 074cd993..760e7e46 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -69,7 +69,7 @@ enum ConnState { // must_use so return values can't be forgotten in Conn::dispatch_packet #[must_use] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub(crate) enum DispatchEvent { /// Incoming channel data Data(channel::DataIn), @@ -80,15 +80,10 @@ pub(crate) enum DispatchEvent { /// Connection state has changed, should poll again Progressed, /// No event + #[default] None, } -impl Default for DispatchEvent { - fn default() -> Self { - Self::None - } -} - impl DispatchEvent { pub fn take(&mut self) -> Self { core::mem::replace(self, DispatchEvent::None) @@ -115,10 +110,7 @@ impl DispatchEvent { } pub(crate) fn is_event(&self) -> bool { - match self { - Self::CliEvent(_) | Self::ServEvent(_) => true, - _ => false, - } + matches!(self, Self::CliEvent(_) | Self::ServEvent(_)) } } @@ -706,7 +698,7 @@ impl Conn { if auth.authed && matches!(self.state, ConnState::PreAuth) { self.state = ConnState::Authed; } - return Ok(()); + Ok(()) } pub(crate) fn resume_servauth_pkok( diff --git a/src/encrypt.rs b/src/encrypt.rs index 40cc3a99..b5bcc3c1 100644 --- a/src/encrypt.rs +++ b/src/encrypt.rs @@ -420,7 +420,7 @@ impl KeysRecv { let sublength = if self.cipher.is_aead() { SSH_LENGTH_SIZE } else { 0 }; let len = buf.len() - size_integ - sublength; - if len % size_block != 0 { + if !len.is_multiple_of(size_block) { debug!("Bad packet, not multiple of block size"); return error::SSHProto.fail(); } diff --git a/src/kex.rs b/src/kex.rs index 9b6fd3f5..e01aad57 100644 --- a/src/kex.rs +++ b/src/kex.rs @@ -346,7 +346,7 @@ impl Kex { } let kex_hash = KexHash::new::( algo_conf, - &our_cookie, + our_cookie, remote_version, &remote_kexinit.into(), )?; @@ -509,11 +509,11 @@ impl Kex { } pub fn is_strict(&self) -> bool { - match self { - Kex::KexDH { algos: Algos { strict_kex: true, .. }, .. } => true, - Kex::NewKeys { algos: Algos { strict_kex: true, .. }, .. } => true, - _ => false, - } + matches!( + self, + Kex::KexDH { algos: Algos { strict_kex: true, .. }, .. } + | Kex::NewKeys { algos: Algos { strict_kex: true, .. }, .. } + ) } pub fn handle_kexdhreply(&self) -> Result { diff --git a/src/lib.rs b/src/lib.rs index 18a850a1..e7c0ad2d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,8 @@ #![allow(unused_imports)] // Static allocations hit this inherently. -#[allow(clippy::large_enum_variant)] +#![allow(clippy::large_enum_variant)] + pub mod config; pub mod packets; pub mod sshnames; diff --git a/src/runner.rs b/src/runner.rs index 10371118..2cd4d791 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -386,7 +386,7 @@ impl<'a, CS: CliServ> Runner<'a, CS> { pub(crate) fn packet(&self) -> Result>> { if let Some((payload, _seq)) = self.traf_in.payload() { - self.conn.packet(payload).map(|p| Some(p)) + self.conn.packet(payload).map(Some) } else { Ok(None) } @@ -502,7 +502,7 @@ impl<'a, CS: CliServ> Runner<'a, CS> { let len = self.write_channel_ready(chan, dt)?; let len = match len { - Some(l) if l == 0 => return Ok(0), + Some(0) => return Ok(0), Some(l) => l, None => return Err(Error::ChannelEOF), }; @@ -826,7 +826,9 @@ pub(crate) fn set_waker(store_waker: &mut Option, new_waker: &Waker) { } } - store_waker.take().map(|w| w.wake()); + if let Some(w) = store_waker.take() { + w.wake() + } *store_waker = Some(new_waker.clone()) } diff --git a/src/servauth.rs b/src/servauth.rs index 749f026a..77023008 100644 --- a/src/servauth.rs +++ b/src/servauth.rs @@ -212,7 +212,7 @@ impl ServAuth { }; let msg = auth::AuthSigMsg::new(p.clone(), sess_id); - match sig_type.verify(key, &msg, &sig) { + match sig_type.verify(key, &msg, sig) { Ok(()) => true, Err(e) => { trace!("sig failed {e}"); diff --git a/src/sshwire.rs b/src/sshwire.rs index 1733ee5d..39064ab8 100644 --- a/src/sshwire.rs +++ b/src/sshwire.rs @@ -126,7 +126,7 @@ pub fn packet_from_bytes<'a>(b: &'a [u8], ctx: &ParseContext) -> Result SSHSink for EncodeBytes<'a> { return Err(WireError::NoRoom); } // keep the borrow checker happy - let tmp = core::mem::replace(&mut self.target, &mut []); + let tmp = core::mem::take(&mut self.target); let t; (t, self.target) = tmp.split_at_mut(v.len()); t.copy_from_slice(v); diff --git a/src/traffic.rs b/src/traffic.rs index 4d21d5fd..c5101fda 100644 --- a/src/traffic.rs +++ b/src/traffic.rs @@ -445,17 +445,14 @@ impl<'a> TrafOut<'a> { } pub fn consume_output(&mut self, l: usize) { - match self.state { - TxState::Write { ref mut idx, len } => { - let wlen = (len - *idx).min(l); - *idx += wlen; + if let TxState::Write { ref mut idx, len } = self.state { + let wlen = (len - *idx).min(l); + *idx += wlen; - if *idx == len { - // all done, read the next packet - self.state = TxState::Idle - } + if *idx == len { + // all done, read the next packet + self.state = TxState::Idle } - _ => (), } } diff --git a/sshwire-derive/src/lib.rs b/sshwire-derive/src/lib.rs index 43e1da60..8345adc1 100644 --- a/sshwire-derive/src/lib.rs +++ b/sshwire-derive/src/lib.rs @@ -2,6 +2,7 @@ //! //! `SSHWIRE_DEBUG` environment variable can be set at build time //! to write generated files to the `target/` directory. +#![expect(clippy::useless_format)] use std::collections::HashSet; use std::env; diff --git a/stdasync/src/knownhosts.rs b/stdasync/src/knownhosts.rs index b9559c82..9face904 100644 --- a/stdasync/src/knownhosts.rs +++ b/stdasync/src/knownhosts.rs @@ -118,20 +118,18 @@ pub fn check_known_hosts_file( if pubk.algorithm() != known_key.algorithm() { debug!("Line {line}, Ignoring other-format existing key {known_key:?}") + } else if pubk.key_data() == known_key.key_data() { + debug!("Line {line}, found matching key"); + return Ok(()); } else { - if pubk.key_data() == known_key.key_data() { - debug!("Line {line}, found matching key"); - return Ok(()); - } else { - let fp = known_key.fingerprint(Default::default()); - println!("\nHost key mismatch for {match_host} in ~/.ssh/known_hosts line {line}\n\ - Existing key has fingerprint {fp}\n"); - return Err(KnownHostsError::Mismatch { - path: p.to_path_buf(), - line, - existing: known_key, - }); - } + let fp = known_key.fingerprint(Default::default()); + println!("\nHost key mismatch for {match_host} in ~/.ssh/known_hosts line {line}\n\ + Existing key has fingerprint {fp}\n"); + return Err(KnownHostsError::Mismatch { + path: p.to_path_buf(), + line, + existing: known_key, + }); } } From 5438ff6d68b362c64d73fdf17fae5cf0f77c1c3a Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 17 Nov 2025 20:31:21 +0800 Subject: [PATCH 176/393] Rename Channels::from_handle_mut to by_handle_mut Reported by clippy::wrong_self_convention --- src/channel.rs | 6 +----- src/runner.rs | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/channel.rs b/src/channel.rs index f8202fb0..e31c89f4 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -101,11 +101,7 @@ impl Channels { } } - pub fn _from_handle(&self, handle: &ChanHandle) -> &Channel { - self.get(handle.0).unwrap() - } - - pub fn from_handle_mut(&mut self, handle: &ChanHandle) -> &mut Channel { + pub fn by_handle_mut(&mut self, handle: &ChanHandle) -> &mut Channel { self.get_mut(handle.0).unwrap() } diff --git a/src/runner.rs b/src/runner.rs index 2cd4d791..b53163e1 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -660,7 +660,7 @@ impl<'a, CS: CliServ> Runner<'a, CS> { dt: ChanData, waker: &Waker, ) { - self.conn.channels.from_handle_mut(ch).set_read_waker( + self.conn.channels.by_handle_mut(ch).set_read_waker( dt, CS::is_client(), waker, @@ -673,7 +673,7 @@ impl<'a, CS: CliServ> Runner<'a, CS> { dt: ChanData, waker: &Waker, ) { - self.conn.channels.from_handle_mut(ch).set_write_waker( + self.conn.channels.by_handle_mut(ch).set_write_waker( dt, CS::is_client(), waker, From 83b56e1680478d1f60598f12671978b24affbda3 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 17 Nov 2025 20:33:35 +0800 Subject: [PATCH 177/393] Replace some #[allow] with #[expect] Some are removed since they are no longer necessary --- src/event.rs | 6 ++---- src/kex.rs | 2 +- src/sshnames.rs | 2 +- stdasync/examples/sunsetc.rs | 6 +++--- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/event.rs b/src/event.rs index 8bd02bb1..fe09592c 100644 --- a/src/event.rs +++ b/src/event.rs @@ -60,7 +60,6 @@ pub enum CliEvent<'g, 'a> { // ChanRequest(ChanRequest<'g, 'a>), // Banner { banner: TextString<'a>, language: TextString<'a> }, /// The SSH connection is no longer running - #[allow(unused)] Defunct, /// No event was returned. @@ -198,7 +197,7 @@ pub(crate) enum CliEventId { SessionOpened(ChanNum), SessionExit, Banner, - #[allow(unused)] + #[expect(unused)] Defunct, // TODO: // Disconnected @@ -306,7 +305,6 @@ pub enum ServEvent<'g, 'a> { SessionEnv(ServEnvironmentRequest<'g, 'a>), /// The SSH session is no longer running - #[allow(unused)] Defunct, /// No event was returned. @@ -942,7 +940,7 @@ pub(crate) enum ServEventId { Environment { num: ChanNum, }, - #[allow(unused)] + #[expect(unused)] Defunct, // TODO: // Disconnected diff --git a/src/kex.rs b/src/kex.rs index e01aad57..89daf1da 100644 --- a/src/kex.rs +++ b/src/kex.rs @@ -106,7 +106,7 @@ impl AlgoConfig { } /// The current state of the Kex -#[allow(clippy::enum_variant_names)] +#[expect(clippy::enum_variant_names)] #[derive(Debug)] pub(crate) enum Kex { /// No key exchange in progress diff --git a/src/sshnames.rs b/src/sshnames.rs index 4474aa57..5826f6b4 100644 --- a/src/sshnames.rs +++ b/src/sshnames.rs @@ -82,7 +82,7 @@ pub enum ChanFail { /// SSH agent message numbers /// /// [draft-miller-ssh-agent](https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent-14#section-5.1) -#[allow(non_camel_case_types)] +#[expect(non_camel_case_types)] #[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] pub enum AgentMessageNum { SSH_AGENT_FAILURE = 5, diff --git a/stdasync/examples/sunsetc.rs b/stdasync/examples/sunsetc.rs index 1ead5607..05f0bbc2 100644 --- a/stdasync/examples/sunsetc.rs +++ b/stdasync/examples/sunsetc.rs @@ -188,17 +188,17 @@ struct Args { cmd: Vec, // options for compatibility with sshfs, are ignored - #[allow(unused)] + #[expect(unused)] #[argh(switch, short = 'x', hidden_help)] /// no X11 no_x11: bool, - #[allow(unused)] + #[expect(unused)] #[argh(switch, short = 'a', hidden_help)] /// no agent forwarding no_agent: bool, - #[allow(unused)] + #[expect(unused)] #[argh(switch, short = '2', hidden_help)] /// ssh version 2 version_2: bool, From cf1971e5b224d8bc91436c5aca7cab8fc3725bf3 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 17 Nov 2025 20:33:59 +0800 Subject: [PATCH 178/393] Small documentation fixes --- README.md | 5 +++-- src/config.rs | 2 +- src/kex.rs | 2 ++ src/sign.rs | 1 - stdasync/src/knownhosts.rs | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b92559d7..9549ac76 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,13 @@ Working: - hmac-sha256 integrity - rsa (`std`-only unless someone writes a `no_std` crate) - `~.` client escape sequences +- Post quantum hybrid key exchange (mlkem) Desirable: - SFTP +- sntrup761 - TCP forwarding -- Post quantum hybrid key exchange - A std server example - Perhaps aes256-gcm - Perhaps ECDSA, hardware often supports it ahead of ed25519 @@ -61,7 +62,7 @@ Release builds should not panic, instead returning `Error::bug()`. `debug_assert!` is used in some places for invariants during testing or fuzzing. -Some attempts are made to clear sensitive memory after use, but stack copies +Some attempts are made to clear sensitive memory after use, but compiler-generated copies will not be cleared. ## Author diff --git a/src/config.rs b/src/config.rs index 4711ee2c..cef59b5e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -27,7 +27,7 @@ pub const MAX_USERNAME: usize = 31; /// Maximum username for client or server /// -/// 32 is the limit for various Linux APIs like wtmp +/// 31 is the limit for various Linux APIs like wtmp #[cfg(feature = "larger")] pub const MAX_USERNAME: usize = 256; diff --git a/src/kex.rs b/src/kex.rs index 89daf1da..bcbe4bb3 100644 --- a/src/kex.rs +++ b/src/kex.rs @@ -547,6 +547,8 @@ impl Kex { } /// Send NewKeys and switch to next encryption key. + /// + /// To be called in Self::Taken state. fn send_newkeys( &mut self, output: KexOutput, diff --git a/src/sign.rs b/src/sign.rs index f68cf523..076aede4 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -234,7 +234,6 @@ pub enum KeyType { /// /// This may hold the private key part locally /// or potentially send the signing requests to an SSH agent or other entity. -// #[derive(ZeroizeOnDrop, Clone, PartialEq)] #[derive(ZeroizeOnDrop, Clone, PartialEq, Eq)] pub enum SignKey { // TODO: we could just have the 32 byte seed here to save memory, but diff --git a/stdasync/src/knownhosts.rs b/stdasync/src/knownhosts.rs index 9face904..eed49a19 100644 --- a/stdasync/src/knownhosts.rs +++ b/stdasync/src/knownhosts.rs @@ -45,7 +45,7 @@ where const USER_KNOWN_HOSTS: &str = ".ssh/known_hosts"; fn user_known_hosts() -> Result { - // home_dir() works fine on linux. + // home_dir() was undeprecated in 1.87 #[allow(deprecated)] let p = std::env::home_dir().ok_or_else(|| KnownHostsError::Other { msg: "Failed getting home directory".into(), From 083ecc77cfd45c68c08fa1d81d21ec339e1efc94 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 17 Nov 2025 20:34:09 +0800 Subject: [PATCH 179/393] Print too-long username length --- src/servauth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/servauth.rs b/src/servauth.rs index 77023008..dd7cb27f 100644 --- a/src/servauth.rs +++ b/src/servauth.rs @@ -80,7 +80,7 @@ impl ServAuth { match Vec::from_slice(p.username.0) { Result::Ok(u) => self.username = Some(u), Result::Err(_) => { - warn!("Client tried too long username"); + warn!("Client tried too long username, {}", p.username.0.len()); return error::SSHProtoUnsupported.fail(); } } From 0706fbb4975fa73ed8ba88fd391fd55057ea7716 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 17 Nov 2025 20:34:21 +0800 Subject: [PATCH 180/393] Remove unnecessary SSHDecode lifetimes --- src/sshwire.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sshwire.rs b/src/sshwire.rs index 39064ab8..467ef8e6 100644 --- a/src/sshwire.rs +++ b/src/sshwire.rs @@ -585,7 +585,7 @@ pub fn try_as_ascii_str(t: &[u8]) -> WireResult<&str> { try_as_ascii(t).map(AsciiStr::as_str) } -impl<'de: 'a, 'a> SSHDecode<'de> for &'a str { +impl<'de> SSHDecode<'de> for &'de str { fn dec(s: &mut S) -> WireResult where S: SSHSource<'de>, @@ -596,7 +596,7 @@ impl<'de: 'a, 'a> SSHDecode<'de> for &'a str { } } -impl<'de: 'a, 'a> SSHDecode<'de> for &'de AsciiStr { +impl<'de> SSHDecode<'de> for &'de AsciiStr { fn dec(s: &mut S) -> WireResult<&'de AsciiStr> where S: SSHSource<'de>, From 1d45cfb1fe391d493b8d73d2ebdf32be513efdaa Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 17 Nov 2025 21:03:05 +0800 Subject: [PATCH 181/393] Remove unused imports Previously crate-wide rules allowed unused imports during development. --- src/auth.rs | 8 -------- src/channel.rs | 10 +++------- src/cliauth.rs | 17 ++++------------- src/client.rs | 10 ++-------- src/conn.rs | 13 +------------ src/encrypt.rs | 9 ++------- src/error.rs | 11 ++--------- src/event.rs | 9 --------- src/ident.rs | 6 +++--- src/kex.rs | 12 ++++-------- src/lib.rs | 5 +---- src/namelist.rs | 5 ++--- src/packets.rs | 10 ++++------ src/runner.rs | 10 +--------- src/servauth.rs | 5 ++--- src/sign.rs | 15 +++------------ src/ssh_chapoly.rs | 8 ++------ src/sshwire.rs | 4 +--- src/sunsetlog.rs | 2 -- src/test.rs | 3 +-- src/traffic.rs | 6 +----- stdasync/src/agent.rs | 6 ++---- stdasync/src/cmdline_client.rs | 12 +++--------- stdasync/src/fdio.rs | 2 +- stdasync/src/knownhosts.rs | 3 +-- stdasync/src/lib.rs | 2 -- stdasync/src/pty.rs | 2 +- 27 files changed, 47 insertions(+), 158 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 30b10dab..1c144c3f 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -4,16 +4,8 @@ use { log::{debug, error, info, log, trace, warn}, }; -use core::task::{Poll, Waker}; -use heapless::{String, Vec}; - use crate::*; -use client::*; use kex::SessId; -use packets::ParseContext; -use packets::{Packet, Signature, Userauth60}; -use sign::SignKey; -use sshnames::*; use sshwire::{BinString, SSHEncode, WireResult}; /// The message to be signed in a pubkey authentication message, diff --git a/src/channel.rs b/src/channel.rs index e31c89f4..10c66142 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -8,26 +8,22 @@ use { use core::num::NonZeroUsize; use core::task::Waker; -use core::{marker::PhantomData, mem}; -use heapless::{Deque, String, Vec}; +use heapless::{String, Vec}; use crate::{runner::set_waker, *}; use config::*; use conn::DispatchEvent; -use conn::Dispatched; use event::{CliEventId, ServEventId}; use packets::{ - ChannelData, ChannelDataExt, ChannelOpen, ChannelOpenFailure, ChannelOpenType, - ChannelReqType, ChannelRequest, Packet, + ChannelData, ChannelDataExt, ChannelOpen, ChannelOpenType, ChannelReqType, + ChannelRequest, Packet, }; use runner::ChanHandle; use sshnames::*; use sshwire::{BinString, SSHEncodeEnum, TextString}; use traffic::TrafSend; -use snafu::ErrorCompat; - pub(crate) struct Channels { ch: [Option; config::MAX_CHANNELS], is_client: bool, diff --git a/src/cliauth.rs b/src/cliauth.rs index 28589677..d0389c5d 100644 --- a/src/cliauth.rs +++ b/src/cliauth.rs @@ -1,7 +1,4 @@ -use self::{ - conn::DispatchEvent, - event::{CliEvent, CliEventId}, -}; +use self::{conn::DispatchEvent, event::CliEventId}; #[allow(unused_imports)] use { @@ -9,21 +6,15 @@ use { log::{debug, error, info, log, trace, warn}, }; -use core::task::{Poll, Waker}; -use heapless::{String, Vec}; -use pretty_hex::PrettyHex; +use heapless::String; use crate::{packets::UserauthPkOk, *}; use auth::AuthType; -use client::*; use kex::SessId; -use packets::{ - AuthMethod, MessageNumber, MethodPubKey, ParseContext, UserauthRequest, -}; -use packets::{Packet, Signature, Userauth60}; +use packets::{AuthMethod, MethodPubKey, ParseContext}; +use packets::{Packet, Userauth60}; use sign::{OwnedSig, SignKey}; use sshnames::*; -use sshwire::{BinString, Blob}; use traffic::TrafSend; #[derive(Debug)] diff --git a/src/client.rs b/src/client.rs index 3935901c..b4b41949 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,15 +4,9 @@ use { log::{debug, error, info, log, trace, warn}, }; -use snafu::prelude::*; - -use crate::{packets::ChannelOpen, *}; +use crate::*; use cliauth::CliAuth; -use heapless::String; -use packets::{Packet, ParseContext, PubKey}; -use sign::SignKey; -use sshnames::*; -use traffic::TrafSend; +use packets::ParseContext; #[derive(Default, Debug)] pub struct Client { diff --git a/src/conn.rs b/src/conn.rs index 760e7e46..f7c6108a 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -12,21 +12,14 @@ use { log::{debug, error, info, log, trace, warn}, }; -use core::char::MAX; -use core::task::{Poll, Waker}; - -use heapless::Vec; use pretty_hex::PrettyHex; use crate::*; use channel::{Channels, CliSessionExit}; use client::Client; -use config::MAX_CHANNELS; -use event::{CliEvent, ServEvent}; use kex::{AlgoConfig, Kex, SessId}; use packets::{Packet, ParseContext}; use server::Server; -use sshnames::*; use traffic::TrafSend; /// The core state of a SSH instance. @@ -722,8 +715,4 @@ impl Conn { } #[cfg(test)] -mod tests { - use crate::conn::*; - use crate::error::Error; - use crate::sunsetlog::*; -} +mod tests {} diff --git a/src/encrypt.rs b/src/encrypt.rs index b5bcc3c1..34c71ca7 100644 --- a/src/encrypt.rs +++ b/src/encrypt.rs @@ -13,12 +13,8 @@ use core::fmt; use core::fmt::Debug; use core::num::Wrapping; -use aes::{ - cipher::{BlockSizeUser, KeyIvInit, KeySizeUser, StreamCipher}, - Aes256, -}; -use hmac::{Hmac, Mac}; -use pretty_hex::PrettyHex; +use aes::cipher::{BlockSizeUser, KeyIvInit, KeySizeUser, StreamCipher}; +use hmac::Mac; use sha2::Digest as Sha2DigestForTrait; use zeroize::ZeroizeOnDrop; @@ -26,7 +22,6 @@ use crate::*; use kex::{self, SessId}; use ssh_chapoly::SSHChaPoly; use sshnames::*; -use sshwire::hash_mpint; // TODO: check that Ctr32 is sufficient. Should be OK with SSH rekeying. type Aes256Ctr32BE = ctr::Ctr32BE; diff --git a/src/error.rs b/src/error.rs index c9a0f433..a27dcb7d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,12 +2,9 @@ use core::str::Utf8Error; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -use core::fmt; use core::fmt::Arguments; -use snafu::{prelude::*, Backtrace, Location}; - -use heapless::String; +use snafu::prelude::*; use crate::channel::ChanNum; @@ -299,8 +296,4 @@ impl From for Error { } #[cfg(test)] -pub(crate) mod tests { - use crate::error::*; - use crate::packets::Unknown; - use crate::sunsetlog::init_test_log; -} +pub(crate) mod tests {} diff --git a/src/event.rs b/src/event.rs index fe09592c..b26c6ec7 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,11 +1,3 @@ -/// Events used by applications running the SSH connection -/// -/// These include hostkeys, authentication, and shell/command sessions -use self::{ - channel::Channel, - packets::{AuthMethod, MethodPubKey, UserauthRequest}, -}; - #[allow(unused_imports)] use { crate::error::{Error, Result, TrapBug}, @@ -14,7 +6,6 @@ use { }; use core::fmt::Debug; -use core::mem::Discriminant; use crate::*; use channel::{CliSessionExit, CliSessionOpener}; diff --git a/src/ident.rs b/src/ident.rs index 36610246..e9ed8445 100644 --- a/src/ident.rs +++ b/src/ident.rs @@ -1,4 +1,4 @@ -use crate::error::{self, Error, Result, TrapBug}; +use crate::error::{self, Error, Result}; pub(crate) const OUR_VERSION: &[u8] = b"SSH-2.0-Sunset-1"; @@ -164,8 +164,8 @@ impl RemoteVersion { #[rustfmt::skip] mod tests { use crate::ident; - use crate::error::{Error,TrapBug,Result}; - use crate::sunsetlog::init_test_log; + use crate::error::{Error,Result}; + // Tests as a client, allowing leading ignored lines fn test_version(v: &str, split: usize, expect: &str) -> Result { diff --git a/src/kex.rs b/src/kex.rs index bcbe4bb3..65b9149b 100644 --- a/src/kex.rs +++ b/src/kex.rs @@ -18,13 +18,13 @@ use ml_kem::{ kem::{Decapsulate, Encapsulate, EncapsulationKey, Kem}, Ciphertext, EncodedSizeUser, KemCore, MlKem768, MlKem768Params, }; -use rand_core::{CryptoRng, OsRng, RngCore}; +use rand_core::OsRng; use sha2::Sha256; -use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; +use zeroize::ZeroizeOnDrop; use crate::*; use encrypt::{Cipher, Integ, KeysRecv, KeysSend}; -use event::{CliEventId, ServEventId}; +use event::ServEventId; use ident::RemoteVersion; use namelist::{LocalNames, NameList}; use packets::{KexCookie, Packet, PubKey, Signature}; @@ -1048,16 +1048,12 @@ impl KexMlkemX25519 { #[cfg(test)] mod tests { - use pretty_hex::PrettyHex; - use crate::encrypt::{self, KeyState, KeysRecv, KeysSend, SSH_PAYLOAD_START}; - use crate::error::Error; use crate::ident::RemoteVersion; use crate::kex; use crate::kex::*; - use crate::packets::{Packet, ParseContext}; + use crate::packets::Packet; use crate::sunsetlog::init_test_log; - use crate::*; use std::collections::VecDeque; // TODO: diff --git a/src/lib.rs b/src/lib.rs index e7c0ad2d..446e8fad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,9 +8,6 @@ #![forbid(unsafe_code)] // avoids headscratching #![deny(unused_must_use)] -// XXX unused_imports only during dev churn -#![allow(unused_imports)] - // Static allocations hit this inherently. #![allow(clippy::large_enum_variant)] @@ -48,7 +45,7 @@ mod termmodes; mod traffic; use conn::DispatchEvent; -use event::{CliEventId, ServEventId}; +use event::CliEventId; // Application API pub use sshwire::TextString; diff --git a/src/namelist.rs b/src/namelist.rs index c9bc90b4..963a40a7 100644 --- a/src/namelist.rs +++ b/src/namelist.rs @@ -14,7 +14,7 @@ use sunset_sshwire_derive::{SSHDecode, SSHEncode}; use crate::*; use heapless::Vec; -use sshwire::{BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, WireResult}; +use sshwire::{SSHDecode, SSHEncode, SSHSink, SSHSource, WireResult}; // Used for lists of: // - algorithm names @@ -222,8 +222,7 @@ impl LocalNames { #[cfg(test)] mod tests { use crate::namelist::*; - use crate::sunsetlog::init_test_log; - use pretty_hex::PrettyHex; + use std::vec::Vec; #[test] diff --git a/src/packets.rs b/src/packets.rs index 739e8440..6ffe97d2 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -15,7 +15,6 @@ use core::fmt::{Debug, Display}; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; -use heapless::String; use pretty_hex::PrettyHex; use sunset_sshwire_derive::*; @@ -24,10 +23,9 @@ use crate::*; use namelist::NameList; use sign::{OwnedSig, SigType}; use sshnames::*; +use sshwire::SSHEncodeEnum; use sshwire::{BinString, Blob, TextString}; use sshwire::{SSHDecode, SSHEncode, SSHSink, SSHSource, WireError, WireResult}; -use sshwire::{SSHDecodeEnum, SSHEncodeEnum}; -use subtle::ConstantTimeEq; #[cfg(feature = "rsa")] use rsa::traits::PublicKeyParts; @@ -1090,11 +1088,11 @@ messagetypes![ #[cfg(test)] mod tests { use crate::packets::*; - use crate::sshnames::*; - use crate::sshwire::tests::{assert_serialize_equal, test_roundtrip}; + + use crate::packets; + use crate::sshwire::tests::test_roundtrip; use crate::sshwire::{packet_from_bytes, write_ssh}; use crate::sunsetlog::init_test_log; - use crate::{packets, sshwire}; use pretty_hex::PrettyHex; #[test] diff --git a/src/runner.rs b/src/runner.rs index b53163e1..21faf861 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -4,21 +4,13 @@ use { log::{debug, error, info, log, trace, warn}, }; -use core::{ - hash::Hash, - mem::discriminant, - task::{Poll, Waker}, -}; - -use pretty_hex::PrettyHex; +use core::{hash::Hash, mem::discriminant, task::Waker}; -use crate::packets::{Packet, Subsystem}; use crate::*; use channel::{ChanData, ChanNum}; use channel::{CliSessionExit, CliSessionOpener}; use encrypt::KeyState; use event::{CliEvent, CliEventId, Event, ServEvent, ServEventId}; -use packets::{ChannelData, ChannelDataExt}; use traffic::{TrafIn, TrafOut}; use conn::{CliServ, Conn, DispatchEvent, Dispatched}; diff --git a/src/servauth.rs b/src/servauth.rs index dd7cb27f..a4eae5bc 100644 --- a/src/servauth.rs +++ b/src/servauth.rs @@ -6,13 +6,12 @@ use { use crate::sshnames::*; use crate::*; -use event::{CliEvent, ServEventId}; +use event::ServEventId; use kex::SessId; use packets::{AuthMethod, Packet, Userauth60, UserauthPkOk, UserauthRequest}; -use sshwire::{BinString, Blob}; use traffic::TrafSend; -use heapless::{String, Vec}; +use heapless::Vec; /// Server authentication context /// diff --git a/src/sign.rs b/src/sign.rs index 076aede4..460ac7b4 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -8,8 +8,6 @@ use { log::{debug, error, info, log, trace, warn}, }; -use core::ops::Deref; - use ed25519_dalek as dalek; use ed25519_dalek::{Signer, Verifier}; use zeroize::ZeroizeOnDrop; @@ -17,12 +15,12 @@ use zeroize::ZeroizeOnDrop; use crate::*; use packets::{Ed25519PubKey, Ed25519Sig, PubKey, Signature}; use sshnames::*; -use sshwire::{BinString, Blob, SSHEncode}; - -use pretty_hex::PrettyHex; +use sshwire::{Blob, SSHEncode}; use core::mem::discriminant; +// only required for some configurations +#[allow(unused_imports)] use digest::Digest; // TODO remove once we use byupdate. @@ -456,12 +454,5 @@ impl TryFrom for SignKey { #[cfg(test)] pub(crate) mod tests { - - use crate::*; - use packets; - use sign::*; - use sshnames::SSH_NAME_ED25519; - use sunsetlog::init_test_log; - // TODO: tests for sign()/verify() and invalid signatures } diff --git a/src/ssh_chapoly.rs b/src/ssh_chapoly.rs index 028c6ce6..5002fddb 100644 --- a/src/ssh_chapoly.rs +++ b/src/ssh_chapoly.rs @@ -6,16 +6,12 @@ use { log::{debug, error, info, log, trace, warn}, }; -use chacha20::cipher::{ - KeyIvInit, StreamCipher, StreamCipherSeek, StreamCipherSeekCore, -}; +use chacha20::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}; use chacha20::ChaCha20; use digest::KeyInit; -use poly1305::universal_hash::generic_array::GenericArray; -use poly1305::universal_hash::UniversalHash; use poly1305::Poly1305; use subtle::ConstantTimeEq; -use zeroize::{Zeroize, ZeroizeOnDrop}; +use zeroize::ZeroizeOnDrop; use crate::*; use encrypt::SSH_LENGTH_SIZE; diff --git a/src/sshwire.rs b/src/sshwire.rs index 467ef8e6..f79a9dc1 100644 --- a/src/sshwire.rs +++ b/src/sshwire.rs @@ -12,11 +12,9 @@ use { }; use core::convert::AsRef; -use core::fmt::{self, Debug, Display}; +use core::fmt::{Debug, Display}; use core::str::FromStr; -use digest::Output; use pretty_hex::PrettyHex; -use snafu::{prelude::*, Location}; use ascii::{AsAsciiStr, AsciiChar, AsciiStr}; diff --git a/src/sunsetlog.rs b/src/sunsetlog.rs index d9b12263..1e76ff2e 100644 --- a/src/sunsetlog.rs +++ b/src/sunsetlog.rs @@ -1,8 +1,6 @@ #[cfg(test)] use simplelog::{self, LevelFilter, TestLogger}; -pub use ::log::{debug, error, info, log, trace, warn}; - #[cfg(test)] pub fn init_test_log() { let conf = diff --git a/src/test.rs b/src/test.rs index 598f6840..be359595 100644 --- a/src/test.rs +++ b/src/test.rs @@ -3,9 +3,8 @@ mod tests { use crate::error::Error; use crate::packets::*; use crate::packets::{Packet, ParseContext}; + use crate::sshwire; use crate::sshwire::BinString; - use crate::{packets, sshwire}; - use pretty_hex::PrettyHex; use simplelog::{self, LevelFilter, TestLogger}; pub fn init_log() { diff --git a/src/traffic.rs b/src/traffic.rs index c5101fda..7f4b4d14 100644 --- a/src/traffic.rs +++ b/src/traffic.rs @@ -9,13 +9,9 @@ use { use zeroize::Zeroize; use crate::channel::{ChanData, ChanNum}; -use crate::encrypt::{ - KeyState, KeysRecv, KeysSend, SSH_LENGTH_SIZE, SSH_PAYLOAD_START, -}; +use crate::encrypt::{KeyState, KeysRecv, KeysSend, SSH_PAYLOAD_START}; use crate::ident::RemoteVersion; -use crate::packets::Packet; use crate::*; -use pretty_hex::PrettyHex; // TODO: if smoltcp exposed both ends of a CircularBuffer to recv() // we could perhaps just work directly in smoltcp's provided buffer? diff --git a/stdasync/src/agent.rs b/stdasync/src/agent.rs index 99f0005c..62f9b44a 100644 --- a/stdasync/src/agent.rs +++ b/stdasync/src/agent.rs @@ -12,12 +12,10 @@ use tokio::net::UnixStream; use sunset_sshwire_derive::*; -use crate::*; use sshwire::{ - BinString, Blob, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, - WireError, WireResult, + Blob, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, + WireResult, }; -use sshwire::{SSHDecodeEnum, SSHEncodeEnum}; use sunset::sshnames::*; use sunset::sshwire; use sunset::{AuthSigMsg, OwnedSig, PubKey, SignKey, Signature}; diff --git a/stdasync/src/cmdline_client.rs b/stdasync/src/cmdline_client.rs index 46d2830c..5629460f 100644 --- a/stdasync/src/cmdline_client.rs +++ b/stdasync/src/cmdline_client.rs @@ -1,19 +1,16 @@ use embassy_futures::select::Either; -use futures::pin_mut; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; use sunset::event::CliEvent; -use sunset::packets::WinChange; use core::fmt::Debug; -use core::str::FromStr; use std::process::ExitCode; -use sunset::{sshnames, AuthSigMsg, OwnedSig, Pty, SignKey}; -use sunset::{Error, Result, Runner, SessionCommand}; +use sunset::{sshnames, Pty, SignKey}; +use sunset::{Error, Result, SessionCommand}; use sunset_async::*; -use embassy_sync::channel::{Channel, Receiver, Sender}; +use embassy_sync::channel::Channel; use embassy_sync::signal::Signal; use embedded_io_async::{Read as _, Write as _}; use std::collections::VecDeque; @@ -22,9 +19,6 @@ use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; use tokio::signal::unix::{signal, SignalKind}; -use futures::FutureExt; -use futures::{future::Fuse, select_biased}; - use crate::pty::win_size; use crate::AgentClient; use crate::*; diff --git a/stdasync/src/fdio.rs b/stdasync/src/fdio.rs index 5fbbc8f7..8d645e87 100644 --- a/stdasync/src/fdio.rs +++ b/stdasync/src/fdio.rs @@ -8,7 +8,7 @@ use tokio::io::{AsyncRead, AsyncWrite, Interest, ReadBuf}; use std::fs::File; use std::io::Error as IoError; use std::io::{Read, Write}; -use std::os::fd::{AsRawFd, FromRawFd, RawFd}; +use std::os::fd::{AsRawFd, FromRawFd}; use core::pin::Pin; use core::task::{Context, Poll}; diff --git a/stdasync/src/knownhosts.rs b/stdasync/src/knownhosts.rs index eed49a19..01d30490 100644 --- a/stdasync/src/knownhosts.rs +++ b/stdasync/src/knownhosts.rs @@ -1,12 +1,11 @@ #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -use std::fs::{File, OpenOptions}; +use std::fs::File; use std::io; use std::io::{BufRead, Read, Write}; use std::path::{Path, PathBuf}; -use crate::*; use sunset::packets::PubKey; type OpenSSHKey = ssh_key::PublicKey; diff --git a/stdasync/src/lib.rs b/stdasync/src/lib.rs index 20986076..f80ededc 100644 --- a/stdasync/src/lib.rs +++ b/stdasync/src/lib.rs @@ -6,7 +6,6 @@ //! [`AgentClient`] can communicate with a separate `ssh-agent` for signing. //! //! `sunsetc` example is usable as a day-to-day SSH client on Linux. -#![allow(unused_imports)] // avoid mysterious missing awaits #![deny(unused_must_use)] @@ -27,4 +26,3 @@ pub use cmdline_client::CmdlineClient; pub use agent::AgentClient; // for sshwire derive -use sunset::sshwire; diff --git a/stdasync/src/pty.rs b/stdasync/src/pty.rs index 2ccb6f96..769e4419 100644 --- a/stdasync/src/pty.rs +++ b/stdasync/src/pty.rs @@ -10,7 +10,7 @@ use nix::sys::termios::Termios; use sunset::config::*; use sunset::packets::WinChange; -use sunset::{Pty, Result, Runner}; +use sunset::{Pty, Result}; /// Returns the size of the current terminal pub fn win_size() -> Result { From fedcbdf7d473b7111e5b53544f7bdd5e1bc0018c Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 17 Nov 2025 22:09:28 +0800 Subject: [PATCH 182/393] sunsetc: escape banner sent by the server --- stdasync/src/cmdline_client.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/stdasync/src/cmdline_client.rs b/stdasync/src/cmdline_client.rs index 5629460f..e7fdaf1b 100644 --- a/stdasync/src/cmdline_client.rs +++ b/stdasync/src/cmdline_client.rs @@ -319,7 +319,10 @@ impl CmdlineClient { } } CliEvent::Banner(b) => { - println!("Banner from server:\n{}", b.banner()?) + println!( + "Banner from server:\n{}", + EscapeBanner(b.banner()?) + ) } CliEvent::Defunct => { trace!("break defunct"); @@ -363,6 +366,22 @@ impl CmdlineClient { } } +/// Disallows ascii control characters, allows newlines. +struct EscapeBanner<'a>(&'a str); + +impl core::fmt::Display for EscapeBanner<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for c in self.0.chars() { + if c.is_control() && c != '\n' { + f.write_str(".")?; + } else { + write!(f, "{}", c)?; + } + } + Ok(()) + } +} + fn set_pty_guard(pty_guard: &mut Option) { match raw_pty() { Ok(p) => *pty_guard = Some(p), From 78cf4a38a5568e97b8a2366c7d350221969ca0dd Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 17 Nov 2025 22:50:46 +0800 Subject: [PATCH 183/393] Revert raw_command() to be public Was unintentionally changed. Fixes: e6b1c6aa2a90 ("Add the necessary structs/types for env.."); --- src/event.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/event.rs b/src/event.rs index b26c6ec7..a9842a91 100644 --- a/src/event.rs +++ b/src/event.rs @@ -738,7 +738,7 @@ impl<'g, 'a> ServExecRequest<'g, 'a> { self.raw_command()?.as_str() } - fn raw_command(&self) -> Result> { + pub fn raw_command(&self) -> Result> { self.runner.fetch_servcommand() } From 6edfb9fb9528957fec994858d1f9ee61bdc410cd Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 17 Nov 2025 22:52:00 +0800 Subject: [PATCH 184/393] Fill some unreleased changes --- changelog.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/changelog.md b/changelog.md index c0c039dd..21e859ae 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,37 @@ # Sunset Changelog +# unreleased + +### Added + +- Add server authentication helpers `matches_username()`, + `matches_password()`. + +- Add environment session variable support + +- Add mlkem768x25519 hybrid post-quantum key exchange + Enabled by `mlkem` feature, will soon be default. + +### Fixed + +- Fix public key authentication. Github #30 + +- Don't fail in some circumstances during key exchange when + packets are received in particular order. Github #25, Github #27 + +- Fix a hang where channels wouldn't get woken for more output + after the SSH stream was written out. Github #25 + +- Fix using sshwire-derive outside of sunset + +- Fix winch signal for sunsetc (regression in 0.3.0) + +### Changed + +- Log a better warning when host key signatures fail + +- Improve exit code handling in sunsetc + ## 0.3.0 - 2025-06-16 ### Changed From 7fa2965c2008a9ef68a5c5a5a461fb187c9e2207 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 18 Nov 2025 14:24:20 +1100 Subject: [PATCH 185/393] WIP: Added SFTP Get capability. Chasing unexpected locks After sending a full SSH_FXA_DATA response, sometimes the process stalls and there is no further request from the client. Looks like this happens when the server answer one response behind the last SSH_FXP_READ request. See client verbose output ``` sftp> get ./65kB_random debug3: Looking up ./demo/sftp/std/testing/out/./65kB_random debug3: Sent message fd 3 T:7 I:21 debug3: Received stat reply T:105 I:21 F:0x000f M:100644 Fetching ./demo/sftp/std/testing/out/./65kB_random to 65kB_random debug2: do_download: download remote "./demo/sftp/std/testing/out/./65kB_random" to local "65kB_random" debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./65kB_random" debug3: Sent message fd 3 T:17 I:22 debug3: Received stat reply T:105 I:22 F:0x000f M:100644 debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./65kB_random" debug3: Sent remote message SSH2_FXP_OPEN I:23 P:./demo/sftp/std/testing/out/./65kB_random M:0x0001 65kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) debug2: channel 0: window 1998336 sent adjust 98816 debug3: Received reply T:103 I:24 R:1 debug3: Received data 0 -> 32767 debug3: Request range 32768 -> 65535 (0/2) debug3: Request range 65536 -> 98303 (1/2) debug3: Received reply T:103 I:25 R:2 debug3: Received data 32768 -> 65535 debug3: Finish at 98304 ( 1) ``` --- demo/sftp/std/src/demosftpserver.rs | 103 +++++++++++++++++++++++++--- demo/sftp/std/src/main.rs | 4 +- demo/sftp/std/testing/test_get.sh | 12 +++- sftp/src/proto.rs | 14 ++++ sftp/src/sftperror.rs | 5 ++ sftp/src/sftphandler/sftphandler.rs | 41 ++++++++++- sftp/src/sftpserver.rs | 91 +++++++++++++++++------- 7 files changed, 229 insertions(+), 41 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 2fbbfc01..8979138a 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -3,6 +3,7 @@ use crate::{ demoopaquefilehandle::DemoOpaqueFileHandle, }; +use sunset_sftp::error::SftpResult; use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; use sunset_sftp::protocol::{Attrs, Filename, NameEntry, PFlags, StatusCode}; use sunset_sftp::server::helpers::DirEntriesCollection; @@ -13,9 +14,8 @@ use sunset_sftp::server::{ #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; use std::fs; -use std::{fs::File, os::unix::fs::FileExt, path::Path}; - use std::os::unix::fs::PermissionsExt; +use std::{fs::File, os::unix::fs::FileExt, path::Path}; #[derive(Debug)] pub(crate) enum PrivatePathHandle { @@ -220,18 +220,99 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } } - fn read( + async fn read( &mut self, opaque_file_handle: &DemoOpaqueFileHandle, offset: u64, - _reply: &mut ReadReply<'_, '_>, - ) -> SftpOpResult<()> { - log::error!( - "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", - opaque_file_handle, - offset - ); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + len: u32, + reply: &ReadReply<'_, N>, + ) -> SftpResult<()> { + if let PrivatePathHandle::File(private_file_handle) = self + .handles_manager + .get_private_as_mut_ref(opaque_file_handle) + .ok_or(StatusCode::SSH_FX_FAILURE)? + { + log::debug!( + "SftpServer Read operation: handle = {:?}, filepath = {:?}, offset = {:?}, len = {:?}", + opaque_file_handle, + private_file_handle.path, + offset, + len + ); + let permissions_poxit = private_file_handle.permissions.unwrap_or(0o000); + if (permissions_poxit & 0o444) == 0 { + error!( + "No read permissions for file {:?}", + private_file_handle.path + ); + return Err(StatusCode::SSH_FX_PERMISSION_DENIED.into()); + }; + + let file_len = private_file_handle + .file + .metadata() + .map_err(|err| { + error!("Could not read the file length: {:?}", err); + StatusCode::SSH_FX_FAILURE + })? + .len(); + + if offset >= file_len { + info!("offset is larger than file length, sending EOF"); + reply.send_eof().await.map_err(|err| { + error!("Could not sent EOF: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; + return Ok(()); + } + + let read_len = if file_len >= len as u64 + offset { + len + } else { + warn!("Read operation: length + offset > file length. Clipping ( {:?} + {:?} > {:?})", + len, offset, file_len); + (file_len - offset).try_into().unwrap_or(u32::MAX) + }; + + reply.send_header(offset, read_len).await?; + + const ARBITRARY_BUFFER_LENGTH: usize = 1024; + + let mut read_buff = [0u8; ARBITRARY_BUFFER_LENGTH]; + + let mut running_offset = offset; + let mut remaining = read_len as usize; + + debug!("Starting reading loop: remaining = {}", remaining); + while remaining > 0 { + let next_read_len: usize = remaining.min(read_buff.len()); + debug!("next_read_len = {}", next_read_len); + let br = private_file_handle + .file + .read_at(&mut read_buff[..next_read_len], running_offset) + .map_err(|err| { + error!("read error: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; + debug!("{} bytes readed", br); + reply.send_data(&read_buff[..br.min(remaining)]).await?; + debug!("Read sent {} bytes", br.min(remaining)); + debug!("remaining {} bytes. {} byte read", remaining, br); + + remaining = + remaining.checked_sub(br).ok_or(StatusCode::SSH_FX_FAILURE)?; + debug!( + "after substracting {} bytes, there are {} bytes remaining", + br, remaining + ); + running_offset = running_offset + .checked_add(br as u64) + .ok_or(StatusCode::SSH_FX_FAILURE)?; + } + debug!("Finished sending data"); + return Ok(()); + } + Err(StatusCode::SSH_FX_PERMISSION_DENIED.into()) } fn write( diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 7ddf09a8..7218d6ce 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -205,7 +205,7 @@ async fn listen( #[embassy_executor::main] async fn main(spawner: Spawner) { env_logger::builder() - .filter_level(log::LevelFilter::Debug) + .filter_level(log::LevelFilter::Info) .filter_module( "sunset_demo_sftp_std::demosftpserver", log::LevelFilter::Debug, @@ -213,7 +213,7 @@ async fn main(spawner: Spawner) { .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Debug) .filter_module( "sunset_sftp::sftphandler::sftpoutputchannelhandler", - log::LevelFilter::Trace, + log::LevelFilter::Info, ) // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) diff --git a/demo/sftp/std/testing/test_get.sh b/demo/sftp/std/testing/test_get.sh index d4f007b6..e498fee9 100755 --- a/demo/sftp/std/testing/test_get.sh +++ b/demo/sftp/std/testing/test_get.sh @@ -7,16 +7,21 @@ REMOTE_HOST="192.168.69.2" REMOTE_USER="any" # Define test files -FILES=("512B_random") +FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random") # Generate random data files echo "Generating random data files..." dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null - +dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=65 of=./65kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=256 of=./256kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=1024 of=./1024kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=2048 of=./2048kB_random 2>/dev/null echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." # Upload the files -sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'put ./%s\n' "${FILES[@]}") bye @@ -39,6 +44,7 @@ rm -f -r ./*_random # Download the files sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'get ./%s\n' "${FILES[@]}") + bye EOF diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 8c7b6654..8dc52b30 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -268,6 +268,20 @@ pub struct Data<'a> { pub data: BinString<'a>, } +/// This is the encoded length for the [`Data`] Sftp Response. +/// +/// This considers the Packet type (1), the request ID (4), and the data string +/// length (4) +/// +/// - It excludes explicitly length field for the SftpPacket +/// - It excludes explicitly length of the data string content +/// +/// It is defined a single source of truth for what is the length for the +/// encoded [`SftpPacket::Data`] variant +/// +/// See [Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) +pub(crate) const ENCODED_BASE_DATA_SFTP_PACKET_LENGTH: u32 = 1 + 4 + 4; + /// Struct to hold `SSH_FXP_NAME` response. /// See [SSH_FXP_NAME in Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) #[derive(Debug, SSHEncode, SSHDecode)] diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index cd01f748..8fd251d9 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -61,6 +61,11 @@ impl From for SftpError { SftpError::RequestHolderError(value) } } +// impl From for SftpError { +// fn from(value: FileServerError) -> Self { +// SftpError::FileServerError(value) +// } +// } impl From for WireError { fn from(value: SftpError) -> Self { diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 9716b4bd..bbf2c3ec 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -7,7 +7,7 @@ use crate::proto::{ SftpNum, SftpPacket, Stat, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; -use crate::server::DirReply; +use crate::server::{DirReply, ReadReply}; use crate::sftperror::SftpResult; use crate::sftphandler::sftpoutputchannelhandler::{ SftpOutputPipe, SftpOutputProducer, @@ -659,6 +659,45 @@ where .await?; }; } + SftpPacket::Read(req_id, read) => { + match file_server + .read( + &T::try_from(&read.handle)?, + read.offset, + read.len, + &ReadReply::new(req_id, output_producer), + ) + .await + { + Ok(()) => { + // debug!("List stats for {} is {:?}", path, attrs); + + // output_producer + // .send_packet(&SftpPacket::Attrs(req_id, attrs)) + // .await?; + } + Err(error) => { + error!("Error reading data: {:?}", error); + if let SftpError::FileServerError(status) = error { + output_producer + .send_status( + req_id, + status, + "Could not list attributes", + ) + .await?; + } else { + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_FAILURE, + "Could not list attributes", + ) + .await?; + } + } + } + } SftpPacket::LStat(req_id, LStat { file_path: path }) => { match file_server.stats(false, path.as_str()?) { Ok(attrs) => { diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 5c954443..ab58ce75 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,6 +1,7 @@ -use crate::error::SftpResult; +use crate::error::{SftpError, SftpResult}; use crate::proto::{ - ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, MAX_NAME_ENTRY_SIZE, NameEntry, PFlags, + ENCODED_BASE_DATA_SFTP_PACKET_LENGTH, ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, + MAX_NAME_ENTRY_SIZE, NameEntry, PFlags, }; use crate::server::SftpSink; use crate::sftphandler::SftpOutputProducer; @@ -11,7 +12,6 @@ use crate::{ use sunset::sshwire::SSHEncode; -use core::marker::PhantomData; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; @@ -67,18 +67,21 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } /// Reads from a file that has previously being opened for reading - fn read( + #[allow(unused)] + async fn read( &mut self, opaque_file_handle: &T, offset: u64, - _reply: &mut ReadReply<'_, '_>, - ) -> SftpOpResult<()> { + len: u32, + reply: &ReadReply<'_, N>, + ) -> SftpResult<()> { log::error!( - "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", + "SftpServer Read operation not defined: handle = {:?}, offset = {:?}, len = {:?}", opaque_file_handle, - offset + offset, + len ); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + Err(SftpError::FileServerError(StatusCode::SSH_FX_OP_UNSUPPORTED)) } /// Writes to a file that has previously being opened for writing fn write( @@ -156,25 +159,65 @@ where /// A reference structure passed to the [`SftpServer::read()`] method to /// allow replying with the read data. -pub struct ReadReply<'g, 'a> { - chan: ChanOut<'g, 'a>, +pub struct ReadReply<'g, const N: usize> { + /// The request Id that will be use`d in the response + req_id: ReqId, + + /// Immutable writer + chan_out: &'g SftpOutputProducer<'g, N>, } -impl<'g, 'a> ReadReply<'g, 'a> { - /// **This is a work in progress** - /// - /// Reply with a slice containing the read data - /// It can be called several times to send multiple data chunks +impl<'g, const N: usize> ReadReply<'g, N> { + /// New instances can only be created within the crate. Users can only + /// use other public methods to use it. + pub(crate) fn new( + req_id: ReqId, + chan_out: &'g SftpOutputProducer<'g, N>, + ) -> Self { + ReadReply { req_id, chan_out } + } + + // TODO Make this enforceable + // TODO Document + pub async fn send_header(&self, offset: u64, data_len: u32) -> SftpResult<()> { + debug!( + "ReadReply: Sending header for request id {:?}: offset = {:?}, data length = {:?}", + self.req_id, offset, data_len + ); + let mut s = [0u8; N]; + let mut sink = SftpSink::new(&mut s); + + // Encoding length field + (data_len + ENCODED_BASE_DATA_SFTP_PACKET_LENGTH).enc(&mut sink)?; + // Encoding packet type + 103u8.enc(&mut sink)?; // TODO Replace hack with + // Encoding req_id + self.req_id.enc(&mut sink)?; + // string data length + data_len.enc(&mut sink)?; + + let payload = sink.payload_slice(); + debug!( + "Sending header: len = {:?}, content = {:?}", + payload.len(), + payload + ); + // Sending payload_slice since we are not making use of the sink sftpPacket length calculation + self.chan_out.send_data(payload).await?; + Ok(()) + } + + /// Sends a buffer with data /// - /// **Important**: The first reply should contain the header - #[allow(unused_variables)] - pub fn reply(self, data: &[u8]) {} -} + /// Call this + pub async fn send_data(&self, buff: &[u8]) -> SftpResult<()> { + self.chan_out.send_data(buff).await + } -// TODO Implement correct Channel Out -pub struct ChanOut<'g, 'a> { - _phantom_g: PhantomData<&'g ()>, // 'g look what these might be ChanIO lifetime - _phantom_a: PhantomData<&'a ()>, // a' Why the second lifetime if ChanIO only needs one + /// Sends EOF meaning that there is no more files in the directory + pub async fn send_eof(&self) -> SftpResult<()> { + self.chan_out.send_status(self.req_id, StatusCode::SSH_FX_EOF, "").await + } } /// Uses for [`DirReply`] to: From c210bb6d026115e819d1b57361612e47e491e482 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 18 Nov 2025 15:18:48 +1100 Subject: [PATCH 186/393] SFTP Read stuck: Failing to receive a a read request? Anyone can take a look at this? Following the analysis in the previous commit, it happens that from time to time there is no more data to be read from the SSH Chan_In while the client is waiting for a response. I have documented this with some logs extracts See files failing-get-client.log, failing-get-server.log (sucess-get{client, server}.log provided for reference). In those failing examples, the server looks stuck waiting to receive new bytes from the channel (SFTP: About to read bytes from SSH Channel) During another run, while in the deadlock, I have manually killed the sftp client (`pkill sftp`) and the server logs receives an rx complete and listen again for new connections (get-deadlock-client-killed.log) --- demo/sftp/std/src/demosftpserver.rs | 13 +- demo/sftp/std/src/main.rs | 2 +- .../std/testing/out/failing-get-client.log | 294 +++++++++++ .../std/testing/out/failing-get-server.log | 471 ++++++++++++++++++ .../out/get-deadlock-client-killed.log | 55 ++ .../std/testing/out/success-get-client.log | 308 ++++++++++++ .../std/testing/out/success-get-server.log | 351 +++++++++++++ demo/sftp/std/testing/test_get.sh | 4 +- sftp/src/sftphandler/sftphandler.rs | 1 + 9 files changed, 1490 insertions(+), 9 deletions(-) create mode 100644 demo/sftp/std/testing/out/failing-get-client.log create mode 100644 demo/sftp/std/testing/out/failing-get-server.log create mode 100644 demo/sftp/std/testing/out/get-deadlock-client-killed.log create mode 100644 demo/sftp/std/testing/out/success-get-client.log create mode 100644 demo/sftp/std/testing/out/success-get-server.log diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 8979138a..a78f8055 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -286,7 +286,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { debug!("Starting reading loop: remaining = {}", remaining); while remaining > 0 { let next_read_len: usize = remaining.min(read_buff.len()); - debug!("next_read_len = {}", next_read_len); + trace!("next_read_len = {}", next_read_len); let br = private_file_handle .file .read_at(&mut read_buff[..next_read_len], running_offset) @@ -294,16 +294,17 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { error!("read error: {:?}", err); StatusCode::SSH_FX_FAILURE })?; - debug!("{} bytes readed", br); + trace!("{} bytes readed", br); reply.send_data(&read_buff[..br.min(remaining)]).await?; - debug!("Read sent {} bytes", br.min(remaining)); - debug!("remaining {} bytes. {} byte read", remaining, br); + trace!("Read sent {} bytes", br.min(remaining)); + trace!("remaining {} bytes. {} byte read", remaining, br); remaining = remaining.checked_sub(br).ok_or(StatusCode::SSH_FX_FAILURE)?; - debug!( + trace!( "after substracting {} bytes, there are {} bytes remaining", - br, remaining + br, + remaining ); running_offset = running_offset .checked_add(br as u64) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 7218d6ce..4f65f975 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -210,7 +210,7 @@ async fn main(spawner: Spawner) { "sunset_demo_sftp_std::demosftpserver", log::LevelFilter::Debug, ) - .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Debug) + .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) .filter_module( "sunset_sftp::sftphandler::sftpoutputchannelhandler", log::LevelFilter::Info, diff --git a/demo/sftp/std/testing/out/failing-get-client.log b/demo/sftp/std/testing/out/failing-get-client.log new file mode 100644 index 00000000..3585756c --- /dev/null +++ b/demo/sftp/std/testing/out/failing-get-client.log @@ -0,0 +1,294 @@ +Testing Multiple GETs... +Generating random data files... +Uploading files to any@192.168.69.2... +Connected to 192.168.69.2. +sftp> put ./512B_random +Uploading ./512B_random to ./demo/sftp/std/testing/out/512B_random +512B_random 100% 512 443.9KB/s 00:00 +sftp> put ./16kB_random +Uploading ./16kB_random to ./demo/sftp/std/testing/out/16kB_random +16kB_random 100% 16KB 260.0KB/s 00:00 +sftp> put ./64kB_random +Uploading ./64kB_random to ./demo/sftp/std/testing/out/64kB_random +64kB_random 100% 64KB 255.3KB/s 00:00 +sftp> put ./65kB_random +Uploading ./65kB_random to ./demo/sftp/std/testing/out/65kB_random +65kB_random 100% 65KB 201.3KB/s 00:00 +sftp> +sftp> bye +client_loop: send disconnect: Broken pipe +UPLOAD Test Results: +============= +Upload PASS: 512B_random +Upload PASS: 16kB_random +Upload PASS: 64kB_random +Upload PASS: 65kB_random +Cleaning up original files... +OpenSSH_8.9p1 Ubuntu-3ubuntu0.13, OpenSSL 3.0.2 15 Mar 2022 +debug1: Reading configuration data /home/jubeor/.ssh/config +debug1: Reading configuration data /etc/ssh/ssh_config +debug1: /etc/ssh/ssh_config line 19: include /etc/ssh/ssh_config.d/*.conf matched no files +debug1: /etc/ssh/ssh_config line 21: Applying options for * +debug2: resolve_canonicalize: hostname 192.168.69.2 is address +debug3: ssh_connect_direct: entering +debug1: Connecting to 192.168.69.2 [192.168.69.2] port 22. +debug3: set_sock_tos: set socket 3 IP_TOS 0x10 +debug1: Connection established. +debug1: identity file /home/jubeor/.ssh/id_rsa type 0 +debug1: identity file /home/jubeor/.ssh/id_rsa-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa_sk type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa_sk-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519 type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519_sk type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519_sk-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_xmss type -1 +debug1: identity file /home/jubeor/.ssh/id_xmss-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_dsa type -1 +debug1: identity file /home/jubeor/.ssh/id_dsa-cert type -1 +debug1: Local version string SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.13 +debug1: Remote protocol version 2.0, remote software version Sunset-1 +debug1: compat_banner: no match: Sunset-1 +debug2: fd 3 setting O_NONBLOCK +debug1: Authenticating to 192.168.69.2:22 as 'any' +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory +debug3: order_hostkeyalgs: no algorithms matched; accept original +debug3: send packet: type 20 +debug1: SSH2_MSG_KEXINIT sent +debug3: receive packet: type 20 +debug1: SSH2_MSG_KEXINIT received +debug2: local client KEXINIT proposal +debug2: KEX algorithms: curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,sntrup761x25519-sha512@openssh.com,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,ext-info-c,kex-strict-c-v00@openssh.com +debug2: host key algorithms: ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,sk-ecdsa-sha2-nistp256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256 +debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com +debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com +debug2: MACs ctos: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 +debug2: MACs stoc: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 +debug2: compression ctos: none,zlib@openssh.com,zlib +debug2: compression stoc: none,zlib@openssh.com,zlib +debug2: languages ctos: +debug2: languages stoc: +debug2: first_kex_follows 0 +debug2: reserved 0 +debug2: peer server KEXINIT proposal +debug2: KEX algorithms: mlkem768x25519-sha256,curve25519-sha256,curve25519-sha256@libssh.org,kex-strict-s-v00@openssh.com,kexguess2@matt.ucc.asn.au +debug2: host key algorithms: ssh-ed25519,rsa-sha2-256 +debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes256-ctr +debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes256-ctr +debug2: MACs ctos: hmac-sha2-256 +debug2: MACs stoc: hmac-sha2-256 +debug2: compression ctos: none +debug2: compression stoc: none +debug2: languages ctos: +debug2: languages stoc: +debug2: first_kex_follows 0 +debug2: reserved 0 +debug3: kex_choose_conf: will use strict KEX ordering +debug1: kex: algorithm: curve25519-sha256 +debug1: kex: host key algorithm: ssh-ed25519 +debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: compression: none +debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: compression: none +debug3: send packet: type 30 +debug1: expecting SSH2_MSG_KEX_ECDH_REPLY +debug3: receive packet: type 31 +debug1: SSH2_MSG_KEX_ECDH_REPLY received +debug1: Server host key: ssh-ed25519 SHA256:6zdZ/bJAJoPDsk3YaJeckvI8Vbl8JmpTVfJH1ldlhyQ +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory +Warning: Permanently added '192.168.69.2' (ED25519) to the list of known hosts. +debug3: send packet: type 21 +debug1: ssh_packet_send2_wrapped: resetting send seqnr 3 +debug2: ssh_set_newkeys: mode 1 +debug1: rekey out after 134217728 blocks +debug1: SSH2_MSG_NEWKEYS sent +debug1: expecting SSH2_MSG_NEWKEYS +debug3: receive packet: type 21 +debug1: ssh_packet_read_poll2: resetting read seqnr 3 +debug1: SSH2_MSG_NEWKEYS received +debug2: ssh_set_newkeys: mode 0 +debug1: rekey in after 134217728 blocks +debug1: Will attempt key: /home/jubeor/.ssh/id_rsa RSA SHA256:Wc43s4t4VZ5DkRa7At3RMd9E0u/UH4CV75mWxI0G8A4 +debug1: Will attempt key: /home/jubeor/.ssh/id_ecdsa +debug1: Will attempt key: /home/jubeor/.ssh/id_ecdsa_sk +debug1: Will attempt key: /home/jubeor/.ssh/id_ed25519 +debug1: Will attempt key: /home/jubeor/.ssh/id_ed25519_sk +debug1: Will attempt key: /home/jubeor/.ssh/id_xmss +debug1: Will attempt key: /home/jubeor/.ssh/id_dsa +debug2: pubkey_prepare: done +debug3: send packet: type 5 +debug3: receive packet: type 7 +debug1: SSH2_MSG_EXT_INFO received +debug1: kex_input_ext_info: server-sig-algs= +debug3: receive packet: type 6 +debug2: service_accept: ssh-userauth +debug1: SSH2_MSG_SERVICE_ACCEPT received +debug3: send packet: type 50 +debug3: receive packet: type 52 +Authenticated to 192.168.69.2 ([192.168.69.2]:22) using "none". +debug1: channel 0: new [client-session] +debug3: ssh_session2_open: channel_new: 0 +debug2: channel 0: send open +debug3: send packet: type 90 +debug1: Entering interactive session. +debug1: pledge: network +debug3: receive packet: type 91 +debug2: channel_input_open_confirmation: channel 0: callback start +debug2: fd 3 setting TCP_NODELAY +debug3: set_sock_tos: set socket 3 IP_TOS 0x08 +debug2: client_session2_setup: id 0 +debug1: Sending environment. +debug3: Ignored env SHELL +debug3: Ignored env USER_ZDOTDIR +debug3: Ignored env COLORTERM +debug3: Ignored env WSL2_GUI_APPS_ENABLED +debug3: Ignored env TERM_PROGRAM_VERSION +debug3: Ignored env WSL_DISTRO_NAME +debug3: Ignored env NAME +debug3: Ignored env PWD +debug3: Ignored env NIX_PROFILES +debug3: Ignored env LOGNAME +debug3: Ignored env VSCODE_GIT_ASKPASS_NODE +debug3: Ignored env VSCODE_INJECTION +debug3: Ignored env HOME +debug1: channel 0: setting env LANG = "C.UTF-8" +debug2: channel 0: request env confirm 0 +debug3: send packet: type 98 +debug3: Ignored env WSL_INTEROP +debug3: Ignored env LS_COLORS +debug3: Ignored env WAYLAND_DISPLAY +debug3: Ignored env NIX_SSL_CERT_FILE +debug3: Ignored env GIT_ASKPASS +debug3: Ignored env VSCODE_GIT_ASKPASS_EXTRA_ARGS +debug3: Ignored env VSCODE_PYTHON_AUTOACTIVATE_GUARD +debug3: Ignored env TERM +debug3: Ignored env ZDOTDIR +debug3: Ignored env USER +debug3: Ignored env VSCODE_GIT_IPC_HANDLE +debug3: Ignored env DISPLAY +debug3: Ignored env SHLVL +debug3: Ignored env XDG_RUNTIME_DIR +debug3: Ignored env WSLENV +debug3: Ignored env VSCODE_GIT_ASKPASS_MAIN +debug3: Ignored env XDG_DATA_DIRS +debug3: Ignored env PATH +debug3: Ignored env DBUS_SESSION_BUS_ADDRESS +debug3: Ignored env HOSTTYPE +debug3: Ignored env PULSE_SERVER +debug3: Ignored env OLDPWD +debug3: Ignored env TERM_PROGRAM +debug3: Ignored env VSCODE_IPC_HOOK_CLI +debug3: Ignored env _ +debug1: Sending subsystem: sftp +debug2: channel 0: request subsystem confirm 1 +debug3: send packet: type 98 +debug2: channel_input_open_confirmation: channel 0: callback done +debug2: channel 0: open confirm rwindow 1000 rmax 1000 +debug3: receive packet: type 99 +debug2: channel_input_status_confirm: type 99 id 0 +debug2: subsystem request accepted on channel 0 +debug2: Remote version: 3 +Connected to 192.168.69.2. +debug2: Sending SSH2_FXP_REALPATH "." +debug3: Sent message fd 3 T:16 I:1 +debug3: SSH2_FXP_REALPATH . -> ./demo/sftp/std/testing/out/ +sftp> get ./512B_random +debug3: Looking up ./demo/sftp/std/testing/out/./512B_random +debug3: Sent message fd 3 T:7 I:2 +debug3: Received stat reply T:105 I:2 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./512B_random to 512B_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./512B_random" to local "512B_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./512B_random" +debug3: Sent message fd 3 T:17 I:3 +debug3: Received stat reply T:105 I:3 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./512B_random" +debug3: Sent remote message SSH2_FXP_OPEN I:4 P:./demo/sftp/std/testing/out/./512B_random M:0x0001 +512B_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug3: Received reply T:103 I:5 R:1 +debug3: Received data 0 -> 511 +debug3: Short data block, re-requesting 512 -> 32767 ( 1) +debug3: Finish at 32768 ( 1) +debug3: Received reply T:101 I:6 R:1 +512B_random 100% 512 11.0KB/s 00:00 +debug3: Sent message SSH2_FXP_CLOSE I:7 +debug3: SSH2_FXP_STATUS 0 +sftp> get ./16kB_random +debug3: Looking up ./demo/sftp/std/testing/out/./16kB_random +debug3: Sent message fd 3 T:7 I:8 +debug3: Received stat reply T:105 I:8 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./16kB_random to 16kB_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./16kB_random" to local "16kB_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./16kB_random" +debug3: Sent message fd 3 T:17 I:9 +debug2: channel 0: rcvd adjust 516 +debug3: Received stat reply T:105 I:9 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./16kB_random" +debug3: Sent remote message SSH2_FXP_OPEN I:10 P:./demo/sftp/std/testing/out/./16kB_random M:0x0001 +16kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug3: Received reply T:103 I:11 R:1 +debug3: Received data 0 -> 16383 +debug3: Short data block, re-requesting 16384 -> 32767 ( 1) +debug3: Finish at 32768 ( 1) +debug3: Received reply T:101 I:12 R:1 +16kB_random 100% 16KB 332.6KB/s 00:00 +debug3: Sent message SSH2_FXP_CLOSE I:13 +debug3: SSH2_FXP_STATUS 0 +sftp> get ./64kB_random +debug3: Looking up ./demo/sftp/std/testing/out/./64kB_random +debug3: Sent message fd 3 T:7 I:14 +debug3: Received stat reply T:105 I:14 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./64kB_random to 64kB_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./64kB_random" to local "64kB_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./64kB_random" +debug3: Sent message fd 3 T:17 I:15 +debug3: Received stat reply T:105 I:15 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./64kB_random" +debug3: Sent remote message SSH2_FXP_OPEN I:16 P:./demo/sftp/std/testing/out/./64kB_random M:0x0001 +64kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug3: Received reply T:103 I:17 R:1 +debug3: Received data 0 -> 32767 +debug3: Request range 32768 -> 65535 (0/2) +debug3: Request range 65536 -> 98303 (1/2) +debug2: channel 0: rcvd adjust 520 +debug3: Received reply T:103 I:18 R:2 +debug3: Received data 32768 -> 65535 +debug3: Finish at 98304 ( 1) +debug3: Received reply T:101 I:19 R:1 +64kB_random 100% 64KB 631.8KB/s 00:00 +debug3: Sent message SSH2_FXP_CLOSE I:20 +debug3: SSH2_FXP_STATUS 0 +sftp> get ./65kB_random +debug3: Looking up ./demo/sftp/std/testing/out/./65kB_random +debug3: Sent message fd 3 T:7 I:21 +debug3: Received stat reply T:105 I:21 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./65kB_random to 65kB_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./65kB_random" to local "65kB_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./65kB_random" +debug3: Sent message fd 3 T:17 I:22 +debug3: Received stat reply T:105 I:22 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./65kB_random" +debug3: Sent remote message SSH2_FXP_OPEN I:23 P:./demo/sftp/std/testing/out/./65kB_random M:0x0001 +65kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug2: channel 0: window 1998336 sent adjust 98816 +debug3: Received reply T:103 I:24 R:1 +debug3: Received data 0 -> 32767 +debug3: Request range 32768 -> 65535 (0/2) +debug3: Request range 65536 -> 98303 (1/2) +debug3: Received reply T:103 I:25 R:2 +debug3: Received data 32768 -> 65535 +debug3: Finish at 98304 ( 1) +65kB_random 98% 64KB 20.1KB/s - stalled -debug3: send packet: type 1 +debug1: channel 0: free: client-session, nchannels 1 +debug3: channel 0: status: The following connections are open: + #0 client-session (t4 r0 i0/0 o0/0 e[write]/0 fd 4/5/6 sock -1 cc -1 io 0x01/0x02) + +Killed by signal 15. +DOWNLOAD Test Results: +============= +Download PASS: 512B_random +Download PASS: 16kB_random +Download PASS: 64kB_random +Download FAIL: 65kB_random +Cleaning up local files... \ No newline at end of file diff --git a/demo/sftp/std/testing/out/failing-get-server.log b/demo/sftp/std/testing/out/failing-get-server.log new file mode 100644 index 00000000..efcac68d --- /dev/null +++ b/demo/sftp/std/testing/out/failing-get-server.log @@ -0,0 +1,471 @@ + 242, 216, 97, 231, 206, 41, 26, 80, 23, 166, 87, 216, 71, 123, 89, 97, 13, 199, 48, 33, 113, 221, 215, 37, 191, 200, 105, 224, 126, 243, 195, 183, 249, 7, 181, 145, 177, 255, 191, 135, 63, 24, 79, 236, 154, 162, 111, 118, 173, 93, 105, 233, 165, 81, 252, 183, 178, 8, 89, 191, 208, 158, 58, 150, 148, 175, 123, 117, 76, 252, 132, 135, 210, 65, 225, 245, 205, 211, 119, 7, 162, 181, 9, 224, 16, 97, 113, 255, 163, 84, 159, 139, 152, 113, 156, 196, 208, 178, 102, 173, 138, 4, 174, 6, 74, 210, 227, 38, 158, 122, 70, 62, 207, 185, 90, 30, 94, 48, 244, 145, 219, 154, 137, 163, 146, 154, 81, 17, 240, 14, 133, 160, 92, 248, 220, 243, 200, 97, 171, 56, 207, 137, 200, 51, 113, 22, 128, 241, 124, 167, 164, 18, 66, 253, 20, 120, 108, 25, 111, 86, 98, 98, 154, 185, 90, 211, 14, 129, 62, 167, 64, 58, 232, 195, 55, 137, 253, 147, 95, 45, 119, 228, 8, 220, 124, 7, 99, 56, 197, 234, 125, 126, 38, 4, 127, 208, 155, 203, 67, 197, 164, 116, 128, 106, 184, 41, 196, 192, 68, 13, 60, 13, 230, 212, 41, 210, 184, 19, 41, 88, 67, 141, 173, 227, 3, 34, 235, 144, 121, 169, 154, 4, 242, 223, 135, 158, 9, 100, 131, 238, 86, 166, 145, 180, 228, 134, 186, 136, 120, 121, 2, 229, 236, 171, 22, 1, 87, 205, 37, 121, 230, 77, 235, 100, 164, 205, 161, 12, 132, 179, 0, 246, 110, 117, 253, 104, 126, 163, 120, 161, 59, 192, 76, 157, 5, 49, 211, 147, 53], index: 0 } +[2025-11-18T04:07:30.367537929Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 63261, data_segment = BinString(len=512), data remaining = 1763 +[2025-11-18T04:07:30.367572804Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 63261, buffer length = 512, bytes written = 512 +[2025-11-18T04:07:30.367601266Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.367626578Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.367653249Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.367687599Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 296 bytes +[2025-11-18T04:07:30.367695505Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [119, 84, 132, 32, 133, 174, 0, 64, 218, 17, 73, 172, 110, 238, 156, 70, 127, 134, 71, 123, 38, 185, 224, 56, 114, 211, 128, 228, 225, 148, 170, 76, 132, 210, 119, 22, 27, 53, 253, 86, 156, 155, 85, 159, 225, 43, 147, 119, 153, 139, 40, 47, 81, 120, 2, 215, 220, 83, 255, 172, 85, 142, 3, 246, 159, 201, 214, 31, 73, 235, 27, 61, 194, 140, 67, 172, 63, 181, 2, 77, 73, 80, 223, 36, 211, 56, 118, 230, 42, 240, 247, 31, 246, 180, 85, 18, 139, 43, 212, 249, 186, 165, 123, 231, 63, 85, 187, 254, 117, 192, 165, 168, 50, 3, 237, 9, 71, 21, 152, 251, 27, 37, 123, 12, 252, 22, 58, 234, 165, 61, 196, 235, 84, 142, 243, 153, 116, 27, 141, 101, 66, 49, 107, 224, 222, 209, 225, 221, 34, 67, 114, 32, 216, 191, 120, 72, 61, 154, 61, 209, 194, 154, 230, 167, 33, 27, 233, 223, 155, 137, 137, 147, 246, 138, 1, 168, 103, 52, 84, 156, 65, 158, 146, 103, 43, 212, 248, 225, 111, 109, 4, 189, 0, 197, 1, 59, 160, 31, 220, 171, 113, 169, 68, 137, 55, 132, 249, 136, 198, 214, 151, 135, 212, 51, 142, 160, 46, 149, 105, 36, 194, 159, 110, 30, 42, 175, 29, 215, 148, 218, 252, 53, 182, 143, 11, 119, 159, 133, 124, 117, 141, 104, 63, 94, 156, 244, 152, 190, 137, 146, 67, 41, 50, 84, 161, 139, 230, 247, 234, 223, 223, 80, 172, 62, 163, 53, 81, 162, 190, 106, 89, 34, 238, 187, 133, 4, 131, 195, 163, 221, 27, 227, 231, 189, 8, 153, 142, 137, 118, 0, 144, 124, 101, 104, 14, 119] +[2025-11-18T04:07:30.367738605Z TRACE sunset_sftp::sftphandler::sftphandler] Received 296 bytes to process +[2025-11-18T04:07:30.367745676Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.367749403Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 296 +[2025-11-18T04:07:30.367773335Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [119, 84, 132, 32, 133, 174, 0, 64, 218, 17, 73, 172, 110, 238, 156, 70, 127, 134, 71, 123, 38, 185, 224, 56, 114, 211, 128, 228, 225, 148, 170, 76, 132, 210, 119, 22, 27, 53, 253, 86, 156, 155, 85, 159, 225, 43, 147, 119, 153, 139, 40, 47, 81, 120, 2, 215, 220, 83, 255, 172, 85, 142, 3, 246, 159, 201, 214, 31, 73, 235, 27, 61, 194, 140, 67, 172, 63, 181, 2, 77, 73, 80, 223, 36, 211, 56, 118, 230, 42, 240, 247, 31, 246, 180, 85, 18, 139, 43, 212, 249, 186, 165, 123, 231, 63, 85, 187, 254, 117, 192, 165, 168, 50, 3, 237, 9, 71, 21, 152, 251, 27, 37, 123, 12, 252, 22, 58, 234, 165, 61, 196, 235, 84, 142, 243, 153, 116, 27, 141, 101, 66, 49, 107, 224, 222, 209, 225, 221, 34, 67, 114, 32, 216, 191, 120, 72, 61, 154, 61, 209, 194, 154, 230, 167, 33, 27, 233, 223, 155, 137, 137, 147, 246, 138, 1, 168, 103, 52, 84, 156, 65, 158, 146, 103, 43, 212, 248, 225, 111, 109, 4, 189, 0, 197, 1, 59, 160, 31, 220, 171, 113, 169, 68, 137, 55, 132, 249, 136, 198, 214, 151, 135, 212, 51, 142, 160, 46, 149, 105, 36, 194, 159, 110, 30, 42, 175, 29, 215, 148, 218, 252, 53, 182, 143, 11, 119, 159, 133, 124, 117, 141, 104, 63, 94, 156, 244, 152, 190, 137, 146, 67, 41, 50, 84, 161, 139, 230, 247, 234, 223, 223, 80, 172, 62, 163, 53, 81, 162, 190, 106, 89, 34, 238, 187, 133, 4, 131, 195, 163, 221, 27, 227, 231, 189, 8, 153, 142, 137, 118, 0, 144, 124, 101, 104, 14, 119], index: 0 } +[2025-11-18T04:07:30.367815097Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 63773, data_segment = BinString(len=296), data remaining = 1467 +[2025-11-18T04:07:30.367850517Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 63773, buffer length = 296, bytes written = 296 +[2025-11-18T04:07:30.367880493Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.367887678Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.367891291Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.368029411Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 512 bytes +[2025-11-18T04:07:30.368058789Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [113, 234, 27, 32, 83, 239, 165, 127, 228, 129, 35, 216, 119, 156, 12, 52, 182, 135, 68, 235, 78, 105, 16, 151, 208, 75, 115, 123, 195, 209, 205, 173, 188, 123, 138, 41, 24, 230, 5, 226, 49, 210, 105, 38, 251, 40, 237, 78, 93, 10, 180, 33, 163, 36, 200, 142, 218, 88, 44, 24, 156, 107, 53, 39, 151, 180, 78, 240, 24, 169, 189, 142, 208, 186, 107, 0, 4, 52, 157, 138, 144, 177, 85, 164, 194, 57, 186, 52, 19, 84, 66, 243, 212, 220, 69, 223, 198, 87, 59, 166, 101, 17, 118, 177, 182, 164, 58, 141, 76, 98, 61, 2, 156, 97, 83, 189, 160, 233, 239, 235, 252, 68, 151, 106, 63, 18, 247, 139, 116, 13, 136, 63, 93, 42, 146, 111, 60, 73, 146, 112, 43, 199, 92, 166, 218, 147, 184, 60, 23, 38, 66, 104, 84, 222, 155, 98, 39, 81, 177, 170, 82, 147, 191, 212, 239, 58, 210, 141, 64, 101, 83, 24, 11, 71, 17, 100, 8, 232, 222, 37, 132, 218, 132, 204, 37, 89, 0, 208, 60, 205, 186, 143, 171, 76, 141, 123, 34, 26, 8, 199, 107, 57, 43, 194, 208, 225, 46, 125, 7, 45, 222, 43, 16, 112, 52, 109, 229, 28, 90, 143, 60, 181, 125, 24, 84, 119, 26, 36, 73, 12, 38, 13, 5, 163, 99, 45, 209, 100, 219, 228, 33, 204, 235, 109, 217, 209, 203, 39, 224, 121, 156, 154, 168, 78, 225, 57, 130, 148, 68, 85, 228, 76, 156, 184, 237, 37, 186, 230, 222, 133, 232, 192, 56, 20, 170, 130, 101, 0, 151, 225, 89, 183, 54, 253, 152, 178, 74, 226, 12, 8, 35, 240, 72, 49, 150, 212, 159, 233, 159, 57, 59, 27, 104, 248, 1, 169, 119, 179, 171, 128, 206, 94, 49, 155, 211, 200, 163, 185, 157, 196, 149, 161, 75, 198, 53, 98, 38, 190, 222, 250, 123, 16, 203, 175, 109, 8, 82, 43, 172, 30, 195, 126, 193, 14, 75, 214, 219, 208, 126, 166, 91, 151, 88, 88, 37, 195, 136, 59, 88, 122, 95, 236, 25, 145, 164, 136, 71, 51, 48, 86, 65, 226, 213, 19, 110, 233, 76, 81, 99, 255, 108, 157, 7, 240, 217, 219, 166, 50, 81, 93, 139, 183, 90, 76, 109, 144, 63, 158, 245, 22, 187, 221, 251, 220, 176, 195, 183, 207, 89, 79, 180, 121, 243, 111, 58, 179, 166, 171, 110, 62, 207, 167, 9, 183, 220, 49, 158, 232, 76, 15, 48, 249, 42, 247, 164, 209, 229, 90, 86, 38, 203, 145, 241, 40, 238, 17, 59, 47, 199, 146, 111, 200, 42, 207, 33, 106, 121, 248, 198, 162, 25, 52, 94, 34, 158, 218, 98, 185, 90, 222, 35, 131, 170, 193, 90, 202, 155, 38, 108, 84, 60, 166, 195, 124, 104, 104, 26, 78, 124, 141, 120, 86, 228, 214, 9, 250, 136, 225, 14, 23, 145, 67, 43, 39, 218, 43, 11, 170, 135, 85, 122, 108] +[2025-11-18T04:07:30.368118657Z TRACE sunset_sftp::sftphandler::sftphandler] Received 512 bytes to process +[2025-11-18T04:07:30.368126820Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.368132389Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 512 +[2025-11-18T04:07:30.368136826Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [113, 234, 27, 32, 83, 239, 165, 127, 228, 129, 35, 216, 119, 156, 12, 52, 182, 135, 68, 235, 78, 105, 16, 151, 208, 75, 115, 123, 195, 209, 205, 173, 188, 123, 138, 41, 24, 230, 5, 226, 49, 210, 105, 38, 251, 40, 237, 78, 93, 10, 180, 33, 163, 36, 200, 142, 218, 88, 44, 24, 156, 107, 53, 39, 151, 180, 78, 240, 24, 169, 189, 142, 208, 186, 107, 0, 4, 52, 157, 138, 144, 177, 85, 164, 194, 57, 186, 52, 19, 84, 66, 243, 212, 220, 69, 223, 198, 87, 59, 166, 101, 17, 118, 177, 182, 164, 58, 141, 76, 98, 61, 2, 156, 97, 83, 189, 160, 233, 239, 235, 252, 68, 151, 106, 63, 18, 247, 139, 116, 13, 136, 63, 93, 42, 146, 111, 60, 73, 146, 112, 43, 199, 92, 166, 218, 147, 184, 60, 23, 38, 66, 104, 84, 222, 155, 98, 39, 81, 177, 170, 82, 147, 191, 212, 239, 58, 210, 141, 64, 101, 83, 24, 11, 71, 17, 100, 8, 232, 222, 37, 132, 218, 132, 204, 37, 89, 0, 208, 60, 205, 186, 143, 171, 76, 141, 123, 34, 26, 8, 199, 107, 57, 43, 194, 208, 225, 46, 125, 7, 45, 222, 43, 16, 112, 52, 109, 229, 28, 90, 143, 60, 181, 125, 24, 84, 119, 26, 36, 73, 12, 38, 13, 5, 163, 99, 45, 209, 100, 219, 228, 33, 204, 235, 109, 217, 209, 203, 39, 224, 121, 156, 154, 168, 78, 225, 57, 130, 148, 68, 85, 228, 76, 156, 184, 237, 37, 186, 230, 222, 133, 232, 192, 56, 20, 170, 130, 101, 0, 151, 225, 89, 183, 54, 253, 152, 178, 74, 226, 12, 8, 35, 240, 72, 49, 150, 212, 159, 233, 159, 57, 59, 27, 104, 248, 1, 169, 119, 179, 171, 128, 206, 94, 49, 155, 211, 200, 163, 185, 157, 196, 149, 161, 75, 198, 53, 98, 38, 190, 222, 250, 123, 16, 203, 175, 109, 8, 82, 43, 172, 30, 195, 126, 193, 14, 75, 214, 219, 208, 126, 166, 91, 151, 88, 88, 37, 195, 136, 59, 88, 122, 95, 236, 25, 145, 164, 136, 71, 51, 48, 86, 65, 226, 213, 19, 110, 233, 76, 81, 99, 255, 108, 157, 7, 240, 217, 219, 166, 50, 81, 93, 139, 183, 90, 76, 109, 144, 63, 158, 245, 22, 187, 221, 251, 220, 176, 195, 183, 207, 89, 79, 180, 121, 243, 111, 58, 179, 166, 171, 110, 62, 207, 167, 9, 183, 220, 49, 158, 232, 76, 15, 48, 249, 42, 247, 164, 209, 229, 90, 86, 38, 203, 145, 241, 40, 238, 17, 59, 47, 199, 146, 111, 200, 42, 207, 33, 106, 121, 248, 198, 162, 25, 52, 94, 34, 158, 218, 98, 185, 90, 222, 35, 131, 170, 193, 90, 202, 155, 38, 108, 84, 60, 166, 195, 124, 104, 104, 26, 78, 124, 141, 120, 86, 228, 214, 9, 250, 136, 225, 14, 23, 145, 67, 43, 39, 218, 43, 11, 170, 135, 85, 122, 108], index: 0 } +[2025-11-18T04:07:30.368188552Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 64069, data_segment = BinString(len=512), data remaining = 955 +[2025-11-18T04:07:30.368239124Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 64069, buffer length = 512, bytes written = 512 +[2025-11-18T04:07:30.368273546Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.368280690Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.368308998Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.368326868Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 488 bytes +[2025-11-18T04:07:30.368351542Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [30, 162, 101, 210, 19, 161, 7, 70, 0, 249, 69, 185, 69, 174, 89, 60, 224, 5, 231, 15, 112, 230, 188, 13, 163, 229, 95, 109, 190, 203, 13, 27, 149, 186, 49, 213, 60, 21, 55, 117, 175, 176, 199, 127, 150, 103, 170, 98, 236, 108, 53, 0, 141, 149, 147, 93, 30, 186, 241, 62, 42, 179, 53, 182, 36, 238, 104, 247, 219, 14, 185, 174, 221, 254, 204, 253, 131, 239, 54, 168, 40, 199, 247, 66, 128, 198, 239, 181, 197, 147, 226, 6, 120, 94, 13, 254, 249, 34, 67, 37, 150, 162, 148, 142, 101, 255, 66, 49, 175, 41, 95, 2, 200, 122, 30, 225, 183, 169, 178, 245, 128, 88, 10, 100, 60, 24, 43, 206, 157, 96, 40, 148, 162, 205, 4, 188, 161, 220, 38, 223, 44, 198, 203, 128, 211, 105, 63, 123, 101, 112, 182, 242, 44, 61, 118, 77, 83, 169, 254, 246, 133, 204, 39, 72, 217, 137, 107, 146, 7, 247, 146, 91, 227, 66, 136, 217, 78, 0, 93, 65, 241, 147, 89, 28, 115, 188, 115, 16, 51, 174, 222, 174, 7, 207, 29, 196, 179, 165, 14, 73, 246, 22, 255, 207, 174, 142, 188, 254, 252, 52, 108, 126, 207, 235, 250, 54, 76, 11, 160, 131, 89, 51, 232, 136, 251, 233, 190, 177, 246, 250, 5, 153, 215, 16, 133, 94, 31, 76, 173, 115, 95, 75, 176, 165, 58, 235, 133, 181, 238, 47, 28, 60, 226, 101, 24, 173, 147, 104, 5, 227, 135, 76, 94, 106, 89, 4, 255, 221, 149, 10, 39, 206, 227, 219, 248, 118, 178, 227, 170, 98, 192, 119, 43, 181, 99, 4, 18, 120, 43, 247, 109, 147, 82, 119, 91, 52, 12, 98, 144, 170, 8, 33, 247, 97, 115, 76, 89, 177, 171, 51, 167, 206, 176, 199, 142, 104, 191, 248, 168, 202, 231, 87, 47, 63, 90, 123, 80, 82, 44, 249, 130, 173, 50, 78, 162, 252, 178, 58, 201, 104, 45, 132, 214, 141, 103, 128, 22, 149, 7, 186, 109, 250, 200, 53, 28, 157, 139, 158, 163, 235, 211, 242, 148, 110, 73, 230, 37, 193, 202, 56, 210, 44, 224, 44, 120, 199, 217, 248, 91, 224, 119, 213, 1, 30, 145, 5, 201, 170, 228, 231, 175, 137, 56, 64, 71, 126, 255, 195, 200, 0, 250, 25, 12, 82, 108, 136, 146, 163, 50, 142, 151, 120, 161, 201, 165, 164, 22, 223, 164, 194, 154, 165, 217, 75, 231, 79, 125, 233, 109, 155, 160, 245, 105, 121, 28, 199, 169, 155, 227, 222, 244, 79, 86, 49, 134, 30, 102, 194, 104, 238, 17, 210, 62, 225, 78, 104, 43, 139, 59, 83, 5, 9, 115, 149, 208, 135, 109, 189, 248, 227, 45, 51, 243, 135, 78, 236, 38, 99, 206, 132, 217, 195, 9, 227, 65, 137, 89, 116] +[2025-11-18T04:07:30.368409124Z TRACE sunset_sftp::sftphandler::sftphandler] Received 488 bytes to process +[2025-11-18T04:07:30.368416474Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.368420561Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 488 +[2025-11-18T04:07:30.368443330Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [30, 162, 101, 210, 19, 161, 7, 70, 0, 249, 69, 185, 69, 174, 89, 60, 224, 5, 231, 15, 112, 230, 188, 13, 163, 229, 95, 109, 190, 203, 13, 27, 149, 186, 49, 213, 60, 21, 55, 117, 175, 176, 199, 127, 150, 103, 170, 98, 236, 108, 53, 0, 141, 149, 147, 93, 30, 186, 241, 62, 42, 179, 53, 182, 36, 238, 104, 247, 219, 14, 185, 174, 221, 254, 204, 253, 131, 239, 54, 168, 40, 199, 247, 66, 128, 198, 239, 181, 197, 147, 226, 6, 120, 94, 13, 254, 249, 34, 67, 37, 150, 162, 148, 142, 101, 255, 66, 49, 175, 41, 95, 2, 200, 122, 30, 225, 183, 169, 178, 245, 128, 88, 10, 100, 60, 24, 43, 206, 157, 96, 40, 148, 162, 205, 4, 188, 161, 220, 38, 223, 44, 198, 203, 128, 211, 105, 63, 123, 101, 112, 182, 242, 44, 61, 118, 77, 83, 169, 254, 246, 133, 204, 39, 72, 217, 137, 107, 146, 7, 247, 146, 91, 227, 66, 136, 217, 78, 0, 93, 65, 241, 147, 89, 28, 115, 188, 115, 16, 51, 174, 222, 174, 7, 207, 29, 196, 179, 165, 14, 73, 246, 22, 255, 207, 174, 142, 188, 254, 252, 52, 108, 126, 207, 235, 250, 54, 76, 11, 160, 131, 89, 51, 232, 136, 251, 233, 190, 177, 246, 250, 5, 153, 215, 16, 133, 94, 31, 76, 173, 115, 95, 75, 176, 165, 58, 235, 133, 181, 238, 47, 28, 60, 226, 101, 24, 173, 147, 104, 5, 227, 135, 76, 94, 106, 89, 4, 255, 221, 149, 10, 39, 206, 227, 219, 248, 118, 178, 227, 170, 98, 192, 119, 43, 181, 99, 4, 18, 120, 43, 247, 109, 147, 82, 119, 91, 52, 12, 98, 144, 170, 8, 33, 247, 97, 115, 76, 89, 177, 171, 51, 167, 206, 176, 199, 142, 104, 191, 248, 168, 202, 231, 87, 47, 63, 90, 123, 80, 82, 44, 249, 130, 173, 50, 78, 162, 252, 178, 58, 201, 104, 45, 132, 214, 141, 103, 128, 22, 149, 7, 186, 109, 250, 200, 53, 28, 157, 139, 158, 163, 235, 211, 242, 148, 110, 73, 230, 37, 193, 202, 56, 210, 44, 224, 44, 120, 199, 217, 248, 91, 224, 119, 213, 1, 30, 145, 5, 201, 170, 228, 231, 175, 137, 56, 64, 71, 126, 255, 195, 200, 0, 250, 25, 12, 82, 108, 136, 146, 163, 50, 142, 151, 120, 161, 201, 165, 164, 22, 223, 164, 194, 154, 165, 217, 75, 231, 79, 125, 233, 109, 155, 160, 245, 105, 121, 28, 199, 169, 155, 227, 222, 244, 79, 86, 49, 134, 30, 102, 194, 104, 238, 17, 210, 62, 225, 78, 104, 43, 139, 59, 83, 5, 9, 115, 149, 208, 135, 109, 189, 248, 227, 45, 51, 243, 135, 78, 236, 38, 99, 206, 132, 217, 195, 9, 227, 65, 137, 89, 116], index: 0 } +[2025-11-18T04:07:30.368502715Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 64581, data_segment = BinString(len=488), data remaining = 467 +[2025-11-18T04:07:30.368535860Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 64581, buffer length = 488, bytes written = 488 +[2025-11-18T04:07:30.368564271Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.368571476Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.368575131Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.368612126Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 14 bytes +[2025-11-18T04:07:30.368618714Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [79, 151, 215, 196, 156, 81, 173, 114, 208, 142, 112, 168, 70, 47] +[2025-11-18T04:07:30.368623573Z TRACE sunset_sftp::sftphandler::sftphandler] Received 14 bytes to process +[2025-11-18T04:07:30.368627268Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.368654742Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 14 +[2025-11-18T04:07:30.368664325Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [79, 151, 215, 196, 156, 81, 173, 114, 208, 142, 112, 168, 70, 47], index: 0 } +[2025-11-18T04:07:30.368673487Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 65069, data_segment = BinString(len=14), data remaining = 453 +[2025-11-18T04:07:30.368753386Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 65069, buffer length = 14, bytes written = 14 +[2025-11-18T04:07:30.368784236Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.368791421Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.368795086Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.368948431Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 512 bytes +[2025-11-18T04:07:30.368981453Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [51, 48, 211, 251, 123, 62, 184, 143, 102, 28, 100, 189, 149, 133, 208, 99, 112, 51, 90, 49, 127, 117, 113, 251, 150, 32, 222, 249, 164, 238, 182, 58, 200, 251, 63, 166, 129, 42, 16, 27, 155, 118, 111, 156, 170, 83, 147, 96, 16, 135, 228, 202, 225, 207, 227, 129, 111, 153, 59, 137, 93, 120, 155, 127, 165, 32, 45, 126, 42, 180, 161, 130, 173, 137, 0, 44, 152, 71, 153, 187, 88, 70, 44, 90, 194, 229, 205, 226, 107, 35, 228, 4, 72, 26, 44, 6, 130, 85, 74, 95, 23, 120, 210, 44, 142, 67, 230, 192, 251, 64, 222, 179, 17, 39, 11, 90, 184, 62, 199, 118, 197, 103, 238, 203, 0, 210, 218, 4, 110, 109, 195, 180, 149, 174, 50, 85, 200, 146, 71, 64, 119, 229, 224, 204, 67, 93, 30, 53, 39, 78, 55, 169, 2, 152, 160, 181, 186, 69, 150, 131, 49, 220, 50, 22, 128, 181, 28, 133, 159, 111, 3, 212, 222, 173, 248, 6, 78, 94, 175, 110, 166, 116, 14, 243, 172, 48, 194, 85, 156, 219, 89, 252, 47, 247, 26, 137, 141, 27, 226, 10, 188, 247, 103, 245, 192, 145, 215, 119, 119, 223, 169, 18, 42, 245, 18, 213, 77, 70, 212, 128, 70, 111, 102, 202, 83, 216, 77, 2, 146, 161, 189, 143, 5, 25, 44, 22, 44, 31, 20, 15, 69, 174, 91, 168, 251, 133, 150, 245, 76, 196, 42, 0, 183, 253, 192, 173, 190, 216, 98, 113, 14, 251, 131, 91, 40, 139, 113, 45, 252, 171, 143, 27, 164, 57, 63, 165, 234, 86, 114, 105, 93, 68, 27, 92, 53, 255, 127, 21, 247, 250, 170, 246, 28, 186, 163, 119, 57, 13, 86, 194, 6, 142, 179, 104, 227, 235, 143, 149, 25, 90, 184, 157, 140, 240, 203, 77, 148, 139, 129, 210, 169, 73, 83, 248, 95, 147, 146, 132, 68, 4, 179, 189, 190, 156, 140, 8, 117, 132, 218, 190, 113, 11, 122, 87, 179, 143, 127, 5, 217, 191, 236, 249, 206, 50, 197, 13, 18, 20, 48, 194, 110, 185, 88, 238, 80, 139, 81, 105, 182, 17, 8, 99, 219, 195, 69, 136, 150, 70, 243, 102, 180, 79, 85, 79, 232, 164, 62, 16, 184, 194, 10, 138, 219, 17, 121, 214, 218, 128, 197, 242, 166, 78, 162, 8, 242, 214, 227, 163, 87, 90, 212, 139, 153, 182, 117, 201, 31, 241, 131, 123, 150, 8, 130, 40, 111, 147, 203, 15, 229, 228, 33, 232, 39, 48, 85, 178, 154, 158, 92, 28, 69, 161, 56, 9, 7, 252, 171, 208, 98, 144, 55, 171, 20, 0, 0, 4, 25, 6, 0, 0, 0, 12, 0, 0, 0, 4, 215, 124, 9, 14, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 38, 28, 151, 7, 83, 85, 208, 31, 140, 39, 122, 77, 215, 209, 166, 8, 75, 127, 195, 6, 17, 127, 158, 6, 93, 66, 218, 212, 56, 12] +[2025-11-18T04:07:30.369041722Z TRACE sunset_sftp::sftphandler::sftphandler] Received 512 bytes to process +[2025-11-18T04:07:30.369050328Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.369054147Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 512 +[2025-11-18T04:07:30.369076896Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [51, 48, 211, 251, 123, 62, 184, 143, 102, 28, 100, 189, 149, 133, 208, 99, 112, 51, 90, 49, 127, 117, 113, 251, 150, 32, 222, 249, 164, 238, 182, 58, 200, 251, 63, 166, 129, 42, 16, 27, 155, 118, 111, 156, 170, 83, 147, 96, 16, 135, 228, 202, 225, 207, 227, 129, 111, 153, 59, 137, 93, 120, 155, 127, 165, 32, 45, 126, 42, 180, 161, 130, 173, 137, 0, 44, 152, 71, 153, 187, 88, 70, 44, 90, 194, 229, 205, 226, 107, 35, 228, 4, 72, 26, 44, 6, 130, 85, 74, 95, 23, 120, 210, 44, 142, 67, 230, 192, 251, 64, 222, 179, 17, 39, 11, 90, 184, 62, 199, 118, 197, 103, 238, 203, 0, 210, 218, 4, 110, 109, 195, 180, 149, 174, 50, 85, 200, 146, 71, 64, 119, 229, 224, 204, 67, 93, 30, 53, 39, 78, 55, 169, 2, 152, 160, 181, 186, 69, 150, 131, 49, 220, 50, 22, 128, 181, 28, 133, 159, 111, 3, 212, 222, 173, 248, 6, 78, 94, 175, 110, 166, 116, 14, 243, 172, 48, 194, 85, 156, 219, 89, 252, 47, 247, 26, 137, 141, 27, 226, 10, 188, 247, 103, 245, 192, 145, 215, 119, 119, 223, 169, 18, 42, 245, 18, 213, 77, 70, 212, 128, 70, 111, 102, 202, 83, 216, 77, 2, 146, 161, 189, 143, 5, 25, 44, 22, 44, 31, 20, 15, 69, 174, 91, 168, 251, 133, 150, 245, 76, 196, 42, 0, 183, 253, 192, 173, 190, 216, 98, 113, 14, 251, 131, 91, 40, 139, 113, 45, 252, 171, 143, 27, 164, 57, 63, 165, 234, 86, 114, 105, 93, 68, 27, 92, 53, 255, 127, 21, 247, 250, 170, 246, 28, 186, 163, 119, 57, 13, 86, 194, 6, 142, 179, 104, 227, 235, 143, 149, 25, 90, 184, 157, 140, 240, 203, 77, 148, 139, 129, 210, 169, 73, 83, 248, 95, 147, 146, 132, 68, 4, 179, 189, 190, 156, 140, 8, 117, 132, 218, 190, 113, 11, 122, 87, 179, 143, 127, 5, 217, 191, 236, 249, 206, 50, 197, 13, 18, 20, 48, 194, 110, 185, 88, 238, 80, 139, 81, 105, 182, 17, 8, 99, 219, 195, 69, 136, 150, 70, 243, 102, 180, 79, 85, 79, 232, 164, 62, 16, 184, 194, 10, 138, 219, 17, 121, 214, 218, 128, 197, 242, 166, 78, 162, 8, 242, 214, 227, 163, 87, 90, 212, 139, 153, 182, 117, 201, 31, 241, 131, 123, 150, 8, 130, 40, 111, 147, 203, 15, 229, 228, 33, 232, 39, 48, 85, 178, 154, 158, 92, 28, 69, 161, 56, 9, 7, 252, 171, 208, 98, 144, 55, 171, 20, 0, 0, 4, 25, 6, 0, 0, 0, 12, 0, 0, 0, 4, 215, 124, 9, 14, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 38, 28, 151, 7, 83, 85, 208, 31, 140, 39, 122, 77, 215, 209, 166, 8, 75, 127, 195, 6, 17, 127, 158, 6, 93, 66, 218, 212, 56, 12], index: 0 } +[2025-11-18T04:07:30.369135735Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 65083, data_segment = BinString(len=453), data remaining = 0 +[2025-11-18T04:07:30.369175273Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 65083, buffer length = 453, bytes written = 453 +[2025-11-18T04:07:30.369190785Z INFO sunset_sftp::sftphandler::sftphandler] Finished multi part Write Request +[2025-11-18T04:07:30.369213040Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 59 +[2025-11-18T04:07:30.369220102Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 59 +[2025-11-18T04:07:30.369224219Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 59 +[2025-11-18T04:07:30.369231342Z WARN sunset_sftp::sftphandler::sftphandler] RanOut for the SFTP Packet in the source buffer: RanOut +[2025-11-18T04:07:30.369237343Z DEBUG sunset_sftp::sftphandler::sftphandler] Handing Ran out +[2025-11-18T04:07:30.369240936Z DEBUG sunset_sftp::sftphandler::sftphandler] about to decode packet partial write content. Source remaining = 30 +[2025-11-18T04:07:30.369263541Z TRACE sunset_sftp::sftphandler::sftphandler] obscured_file_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, req_id = ReqId(12), offset = 65536, data_in_buffer = BinString(len=30), write_tracker = PartialWriteRequestTracker { req_id: ReqId(12), opaque_handle: DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, remain_data_len: 994, remain_data_offset: 65566 } +[2025-11-18T04:07:30.369340424Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 65536, buffer length = 30, bytes written = 30 +[2025-11-18T04:07:30.369370214Z DEBUG sunset_sftp::sftphandler::sftphandler] Storing a write tracker for a fragmented write request +[2025-11-18T04:07:30.369377760Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.369381640Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.369385377Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.369407447Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.369426902Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 488 bytes +[2025-11-18T04:07:30.369434344Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [43, 55, 242, 47, 60, 147, 5, 18, 108, 226, 171, 31, 250, 134, 222, 88, 76, 18, 140, 139, 149, 232, 117, 190, 108, 79, 56, 233, 54, 103, 151, 155, 205, 115, 56, 64, 125, 215, 131, 193, 19, 235, 187, 62, 122, 202, 67, 106, 202, 176, 8, 166, 97, 192, 98, 233, 183, 176, 101, 151, 34, 94, 88, 158, 124, 92, 59, 162, 124, 42, 84, 163, 159, 153, 89, 183, 17, 80, 90, 208, 121, 44, 98, 90, 234, 161, 214, 153, 182, 129, 199, 61, 48, 225, 184, 134, 84, 31, 218, 9, 70, 10, 169, 22, 148, 205, 229, 245, 191, 103, 33, 248, 164, 79, 183, 60, 97, 40, 205, 135, 36, 92, 210, 36, 51, 48, 222, 160, 34, 171, 94, 3, 107, 196, 119, 104, 214, 186, 93, 2, 198, 137, 161, 252, 237, 103, 31, 152, 123, 147, 68, 38, 116, 172, 152, 98, 63, 112, 225, 112, 195, 216, 142, 18, 15, 201, 222, 138, 9, 189, 255, 56, 244, 113, 202, 217, 52, 223, 59, 67, 216, 147, 137, 167, 28, 253, 149, 78, 161, 231, 117, 151, 2, 5, 12, 90, 247, 201, 243, 237, 236, 64, 229, 53, 198, 248, 250, 87, 113, 247, 237, 50, 232, 95, 140, 16, 231, 6, 182, 157, 250, 57, 128, 217, 42, 0, 82, 218, 88, 238, 182, 96, 105, 6, 245, 0, 184, 139, 121, 85, 43, 144, 23, 164, 136, 15, 224, 9, 127, 228, 148, 104, 196, 14, 79, 187, 139, 128, 100, 102, 4, 15, 155, 22, 13, 133, 116, 67, 192, 249, 126, 96, 133, 14, 98, 22, 124, 17, 36, 207, 206, 195, 143, 180, 54, 31, 111, 190, 193, 250, 19, 70, 47, 86, 70, 170, 227, 164, 9, 186, 26, 3, 148, 1, 83, 111, 234, 57, 144, 198, 82, 141, 15, 65, 239, 71, 85, 182, 87, 45, 19, 252, 18, 244, 212, 40, 170, 155, 124, 71, 109, 242, 40, 198, 39, 75, 230, 215, 179, 76, 186, 192, 165, 34, 87, 155, 108, 22, 129, 62, 178, 239, 88, 229, 113, 194, 133, 134, 108, 239, 161, 90, 234, 184, 100, 124, 58, 124, 189, 14, 195, 242, 88, 170, 182, 224, 239, 54, 117, 94, 71, 240, 9, 0, 240, 249, 36, 80, 180, 134, 9, 139, 22, 4, 71, 132, 42, 95, 103, 132, 203, 60, 33, 214, 1, 33, 116, 98, 71, 168, 49, 166, 103, 103, 221, 147, 159, 128, 54, 147, 110, 3, 163, 122, 106, 118, 239, 224, 233, 153, 28, 227, 22, 195, 141, 7, 131, 239, 164, 148, 97, 52, 17, 181, 126, 252, 187, 188, 62, 27, 36, 28, 29, 81, 173, 28, 31, 245, 127, 214, 172, 170, 104, 116, 4, 45, 176, 188, 148, 148, 177, 84, 21, 159, 161, 92, 159, 107, 40, 119, 130, 160, 70, 129, 209, 192, 106, 72] +[2025-11-18T04:07:30.369488540Z TRACE sunset_sftp::sftphandler::sftphandler] Received 488 bytes to process +[2025-11-18T04:07:30.369495365Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.369499060Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 488 +[2025-11-18T04:07:30.369503363Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [43, 55, 242, 47, 60, 147, 5, 18, 108, 226, 171, 31, 250, 134, 222, 88, 76, 18, 140, 139, 149, 232, 117, 190, 108, 79, 56, 233, 54, 103, 151, 155, 205, 115, 56, 64, 125, 215, 131, 193, 19, 235, 187, 62, 122, 202, 67, 106, 202, 176, 8, 166, 97, 192, 98, 233, 183, 176, 101, 151, 34, 94, 88, 158, 124, 92, 59, 162, 124, 42, 84, 163, 159, 153, 89, 183, 17, 80, 90, 208, 121, 44, 98, 90, 234, 161, 214, 153, 182, 129, 199, 61, 48, 225, 184, 134, 84, 31, 218, 9, 70, 10, 169, 22, 148, 205, 229, 245, 191, 103, 33, 248, 164, 79, 183, 60, 97, 40, 205, 135, 36, 92, 210, 36, 51, 48, 222, 160, 34, 171, 94, 3, 107, 196, 119, 104, 214, 186, 93, 2, 198, 137, 161, 252, 237, 103, 31, 152, 123, 147, 68, 38, 116, 172, 152, 98, 63, 112, 225, 112, 195, 216, 142, 18, 15, 201, 222, 138, 9, 189, 255, 56, 244, 113, 202, 217, 52, 223, 59, 67, 216, 147, 137, 167, 28, 253, 149, 78, 161, 231, 117, 151, 2, 5, 12, 90, 247, 201, 243, 237, 236, 64, 229, 53, 198, 248, 250, 87, 113, 247, 237, 50, 232, 95, 140, 16, 231, 6, 182, 157, 250, 57, 128, 217, 42, 0, 82, 218, 88, 238, 182, 96, 105, 6, 245, 0, 184, 139, 121, 85, 43, 144, 23, 164, 136, 15, 224, 9, 127, 228, 148, 104, 196, 14, 79, 187, 139, 128, 100, 102, 4, 15, 155, 22, 13, 133, 116, 67, 192, 249, 126, 96, 133, 14, 98, 22, 124, 17, 36, 207, 206, 195, 143, 180, 54, 31, 111, 190, 193, 250, 19, 70, 47, 86, 70, 170, 227, 164, 9, 186, 26, 3, 148, 1, 83, 111, 234, 57, 144, 198, 82, 141, 15, 65, 239, 71, 85, 182, 87, 45, 19, 252, 18, 244, 212, 40, 170, 155, 124, 71, 109, 242, 40, 198, 39, 75, 230, 215, 179, 76, 186, 192, 165, 34, 87, 155, 108, 22, 129, 62, 178, 239, 88, 229, 113, 194, 133, 134, 108, 239, 161, 90, 234, 184, 100, 124, 58, 124, 189, 14, 195, 242, 88, 170, 182, 224, 239, 54, 117, 94, 71, 240, 9, 0, 240, 249, 36, 80, 180, 134, 9, 139, 22, 4, 71, 132, 42, 95, 103, 132, 203, 60, 33, 214, 1, 33, 116, 98, 71, 168, 49, 166, 103, 103, 221, 147, 159, 128, 54, 147, 110, 3, 163, 122, 106, 118, 239, 224, 233, 153, 28, 227, 22, 195, 141, 7, 131, 239, 164, 148, 97, 52, 17, 181, 126, 252, 187, 188, 62, 27, 36, 28, 29, 81, 173, 28, 31, 245, 127, 214, 172, 170, 104, 116, 4, 45, 176, 188, 148, 148, 177, 84, 21, 159, 161, 92, 159, 107, 40, 119, 130, 160, 70, 129, 209, 192, 106, 72], index: 0 } +[2025-11-18T04:07:30.369557096Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 65566, data_segment = BinString(len=488), data remaining = 506 +[2025-11-18T04:07:30.369613577Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 65566, buffer length = 488, bytes written = 488 +[2025-11-18T04:07:30.369643882Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.369651088Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.369654845Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.369682967Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 42 bytes +[2025-11-18T04:07:30.369708938Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [115, 168, 108, 51, 193, 52, 19, 31, 170, 36, 253, 14, 65, 50, 49, 88, 211, 185, 67, 14, 180, 123, 6, 104, 161, 36, 215, 59, 30, 39, 183, 67, 103, 153, 2, 76, 248, 171, 77, 162, 79, 167] +[2025-11-18T04:07:30.369740776Z TRACE sunset_sftp::sftphandler::sftphandler] Received 42 bytes to process +[2025-11-18T04:07:30.369748733Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.369752439Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 42 +[2025-11-18T04:07:30.369756762Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [115, 168, 108, 51, 193, 52, 19, 31, 170, 36, 253, 14, 65, 50, 49, 88, 211, 185, 67, 14, 180, 123, 6, 104, 161, 36, 215, 59, 30, 39, 183, 67, 103, 153, 2, 76, 248, 171, 77, 162, 79, 167], index: 0 } +[2025-11-18T04:07:30.369765399Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 66054, data_segment = BinString(len=42), data remaining = 464 +[2025-11-18T04:07:30.369796517Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 66054, buffer length = 42, bytes written = 42 +[2025-11-18T04:07:30.369826502Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.369833800Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.369837475Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.369920432Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 464 bytes +[2025-11-18T04:07:30.369949090Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [144, 252, 138, 11, 49, 93, 92, 70, 91, 200, 15, 151, 171, 209, 193, 224, 198, 108, 107, 33, 29, 247, 54, 131, 42, 129, 145, 69, 168, 65, 246, 87, 69, 140, 137, 171, 22, 223, 208, 133, 172, 54, 208, 168, 11, 51, 51, 179, 124, 207, 104, 39, 26, 140, 189, 192, 72, 134, 18, 86, 242, 95, 119, 47, 243, 30, 227, 180, 119, 13, 139, 55, 115, 92, 38, 251, 204, 89, 227, 98, 170, 32, 2, 125, 129, 13, 111, 180, 77, 82, 18, 191, 146, 148, 39, 244, 34, 36, 15, 90, 60, 66, 118, 143, 150, 42, 246, 59, 152, 69, 178, 174, 173, 41, 41, 68, 17, 226, 96, 133, 142, 228, 252, 133, 80, 178, 183, 235, 54, 61, 148, 149, 158, 75, 241, 91, 229, 204, 120, 136, 66, 74, 158, 61, 38, 251, 175, 36, 236, 179, 192, 212, 53, 193, 162, 84, 244, 35, 85, 102, 36, 144, 242, 62, 10, 150, 161, 26, 156, 177, 244, 27, 58, 164, 187, 51, 126, 214, 2, 200, 186, 136, 58, 175, 113, 104, 38, 80, 155, 57, 11, 74, 72, 98, 199, 24, 249, 108, 59, 102, 33, 160, 171, 168, 13, 144, 143, 16, 65, 55, 135, 101, 233, 213, 228, 138, 189, 140, 16, 90, 33, 198, 133, 32, 183, 7, 154, 61, 57, 43, 241, 29, 43, 191, 50, 246, 249, 50, 177, 21, 49, 237, 228, 249, 224, 33, 213, 95, 81, 35, 23, 33, 208, 114, 41, 183, 18, 19, 185, 2, 127, 224, 158, 228, 32, 244, 0, 38, 240, 149, 51, 113, 46, 55, 17, 35, 50, 39, 139, 115, 238, 30, 74, 26, 226, 56, 57, 178, 159, 49, 61, 204, 98, 27, 123, 37, 131, 102, 179, 61, 36, 221, 15, 14, 51, 100, 221, 89, 122, 82, 172, 98, 143, 114, 159, 181, 153, 235, 235, 94, 217, 5, 92, 123, 66, 186, 40, 184, 153, 74, 229, 109, 251, 39, 206, 18, 151, 65, 191, 233, 187, 105, 71, 197, 178, 252, 152, 113, 179, 236, 147, 105, 8, 123, 95, 238, 190, 244, 251, 11, 87, 114, 178, 3, 240, 183, 3, 171, 171, 17, 213, 63, 74, 239, 129, 235, 127, 198, 35, 210, 87, 233, 28, 225, 194, 57, 27, 50, 1, 181, 87, 118, 206, 140, 130, 230, 134, 190, 166, 172, 98, 229, 152, 112, 114, 177, 122, 102, 255, 55, 31, 52, 28, 237, 35, 205, 67, 103, 159, 224, 134, 181, 42, 59, 142, 12, 30, 121, 177, 101, 146, 78, 249, 246, 175, 107, 205, 154, 36, 230, 89, 16, 110, 26, 87, 247, 195, 243, 39, 245, 190, 176, 241, 153, 183, 18, 146, 245, 68, 239, 209, 23, 63, 62] +[2025-11-18T04:07:30.370005942Z TRACE sunset_sftp::sftphandler::sftphandler] Received 464 bytes to process +[2025-11-18T04:07:30.370012622Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.370016369Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 464 +[2025-11-18T04:07:30.370024100Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [144, 252, 138, 11, 49, 93, 92, 70, 91, 200, 15, 151, 171, 209, 193, 224, 198, 108, 107, 33, 29, 247, 54, 131, 42, 129, 145, 69, 168, 65, 246, 87, 69, 140, 137, 171, 22, 223, 208, 133, 172, 54, 208, 168, 11, 51, 51, 179, 124, 207, 104, 39, 26, 140, 189, 192, 72, 134, 18, 86, 242, 95, 119, 47, 243, 30, 227, 180, 119, 13, 139, 55, 115, 92, 38, 251, 204, 89, 227, 98, 170, 32, 2, 125, 129, 13, 111, 180, 77, 82, 18, 191, 146, 148, 39, 244, 34, 36, 15, 90, 60, 66, 118, 143, 150, 42, 246, 59, 152, 69, 178, 174, 173, 41, 41, 68, 17, 226, 96, 133, 142, 228, 252, 133, 80, 178, 183, 235, 54, 61, 148, 149, 158, 75, 241, 91, 229, 204, 120, 136, 66, 74, 158, 61, 38, 251, 175, 36, 236, 179, 192, 212, 53, 193, 162, 84, 244, 35, 85, 102, 36, 144, 242, 62, 10, 150, 161, 26, 156, 177, 244, 27, 58, 164, 187, 51, 126, 214, 2, 200, 186, 136, 58, 175, 113, 104, 38, 80, 155, 57, 11, 74, 72, 98, 199, 24, 249, 108, 59, 102, 33, 160, 171, 168, 13, 144, 143, 16, 65, 55, 135, 101, 233, 213, 228, 138, 189, 140, 16, 90, 33, 198, 133, 32, 183, 7, 154, 61, 57, 43, 241, 29, 43, 191, 50, 246, 249, 50, 177, 21, 49, 237, 228, 249, 224, 33, 213, 95, 81, 35, 23, 33, 208, 114, 41, 183, 18, 19, 185, 2, 127, 224, 158, 228, 32, 244, 0, 38, 240, 149, 51, 113, 46, 55, 17, 35, 50, 39, 139, 115, 238, 30, 74, 26, 226, 56, 57, 178, 159, 49, 61, 204, 98, 27, 123, 37, 131, 102, 179, 61, 36, 221, 15, 14, 51, 100, 221, 89, 122, 82, 172, 98, 143, 114, 159, 181, 153, 235, 235, 94, 217, 5, 92, 123, 66, 186, 40, 184, 153, 74, 229, 109, 251, 39, 206, 18, 151, 65, 191, 233, 187, 105, 71, 197, 178, 252, 152, 113, 179, 236, 147, 105, 8, 123, 95, 238, 190, 244, 251, 11, 87, 114, 178, 3, 240, 183, 3, 171, 171, 17, 213, 63, 74, 239, 129, 235, 127, 198, 35, 210, 87, 233, 28, 225, 194, 57, 27, 50, 1, 181, 87, 118, 206, 140, 130, 230, 134, 190, 166, 172, 98, 229, 152, 112, 114, 177, 122, 102, 255, 55, 31, 52, 28, 237, 35, 205, 67, 103, 159, 224, 134, 181, 42, 59, 142, 12, 30, 121, 177, 101, 146, 78, 249, 246, 175, 107, 205, 154, 36, 230, 89, 16, 110, 26, 87, 247, 195, 243, 39, 245, 190, 176, 241, 153, 183, 18, 146, 245, 68, 239, 209, 23, 63, 62], index: 0 } +[2025-11-18T04:07:30.370074055Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 66096, data_segment = BinString(len=464), data remaining = 0 +[2025-11-18T04:07:30.370107550Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 66096, buffer length = 464, bytes written = 464 +[2025-11-18T04:07:30.370140253Z INFO sunset_sftp::sftphandler::sftphandler] Finished multi part Write Request +[2025-11-18T04:07:30.370166348Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.370193647Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.370220554Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.414834521Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:07:30.414888388Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 9, 0, 0, 0, 4, 215, 124, 9, 14] +[2025-11-18T04:07:30.414897930Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:07:30.414901863Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.414905599Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:07:30.414910478Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:07:30.414941452Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(9), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:07:30.414973723Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/65kB_random" was successful +[2025-11-18T04:07:30.415018140Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.415049062Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.415056278Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.415060097Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.415629821Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 0 bytes +[2025-11-18T04:07:30.415667815Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [] +[2025-11-18T04:07:30.415675515Z DEBUG sunset_sftp::sftphandler::sftphandler] client disconnected +[2025-11-18T04:07:30.415679293Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing returned: Err(ClientDisconnected) +[2025-11-18T04:07:30.415686128Z WARN sunset_demo_sftp_std] sftp_loop finished: Err(ChannelEOF) +[2025-11-18T04:07:30.415693045Z WARN sunset_demo_common::server] Ended with error ChannelEOF +[2025-11-18T04:07:30.415799060Z INFO sunset_demo_common::server] Listening on TCP:22... +[2025-11-18T04:07:30.444787113Z INFO sunset_demo_common::server] Connection from 192.168.69.100:45016 +[2025-11-18T04:07:30.444866539Z INFO sunset_demo_sftp_std] prog_loop started +[2025-11-18T04:07:30.473277794Z INFO sunset_demo_common::server] Allowing auth for user any +[2025-11-18T04:07:30.475576502Z WARN sunset::channel] Unknown channel req type "env" +[2025-11-18T04:07:30.475678996Z INFO sunset_demo_sftp_std] Starting 'sftp' subsystem +[2025-11-18T04:07:30.475762416Z INFO sunset_demo_sftp_std] SFTP loop has received a channel handle ChanNum(0) +[2025-11-18T04:07:30.475823602Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.475911746Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 9 bytes +[2025-11-18T04:07:30.475967919Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 5, 1, 0, 0, 0, 3] +[2025-11-18T04:07:30.476025265Z TRACE sunset_sftp::sftphandler::sftphandler] Received 9 bytes to process +[2025-11-18T04:07:30.476041055Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.476099441Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Initializing ]=======================> Buffer remaining: 9 +[2025-11-18T04:07:30.476118824Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 9 +[2025-11-18T04:07:30.476173978Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.476229100Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.476416003Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.522213652Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 14 bytes +[2025-11-18T04:07:30.522288240Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 10, 16, 0, 0, 0, 1, 0, 0, 0, 1, 46] +[2025-11-18T04:07:30.522311205Z TRACE sunset_sftp::sftphandler::sftphandler] Received 14 bytes to process +[2025-11-18T04:07:30.522320047Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.522328292Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 14 +[2025-11-18T04:07:30.522381006Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 14 +[2025-11-18T04:07:30.522442943Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: PathInfo(ReqId(1), PathInfo { path: TextString(".") }) +[2025-11-18T04:07:30.522463562Z INFO sunset_demo_sftp_std::demosftpserver] finding path for: "." +[2025-11-18T04:07:30.522478652Z DEBUG sunset_demo_sftp_std::demosftpserver] Will return: NameEntry { filename: Filename(TextString("./demo/sftp/std/testing/out/")), _longname: Filename(TextString("")), attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } } +[2025-11-18T04:07:30.522501411Z DEBUG sunset_sftp::sftphandler::sftphandler] PathInfo encoded length: 40 +[2025-11-18T04:07:30.522514309Z TRACE sunset_sftp::sftphandler::sftphandler] PathInfo Response content: 40 +[2025-11-18T04:07:30.522528391Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.522574733Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.522590472Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.522598738Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.523406710Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.523448306Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 2, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.523460638Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.523464519Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.523468214Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.523495215Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.523525118Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(2), LStat { file_path: TextString("./demo/sftp/std/testing/out/./512B_random") }) +[2025-11-18T04:07:30.523539622Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./512B_random" +[2025-11-18T04:07:30.523563112Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./512B_random" is Attrs { size: Some(512), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438849), ext_count: None } +[2025-11-18T04:07:30.523593499Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.523600653Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.523604276Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.523607889Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.524163861Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.524204675Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 3, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.524217059Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.524221104Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.524225057Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.524230369Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.524260478Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(3), Stat { file_path: TextString("./demo/sftp/std/testing/out/./512B_random") }) +[2025-11-18T04:07:30.524295311Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./512B_random" +[2025-11-18T04:07:30.524328292Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./512B_random" is Attrs { size: Some(512), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438849), ext_count: None } +[2025-11-18T04:07:30.524358340Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.524365411Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.524370023Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.524374789Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.525025730Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:07:30.525065330Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 4, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:07:30.525077909Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:07:30.525081810Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.525085454Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:07:30.525089849Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:07:30.525119032Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(4), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./512B_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:07:30.525154628Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./512B_random", mode = SSH_FXF_READ +[2025-11-18T04:07:30.525181473Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:07:30.525242556Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./512B_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }) +[2025-11-18T04:07:30.525273242Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.525280591Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.525284276Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.525306634Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.526078773Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:07:30.526119083Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 5, 0, 0, 0, 4, 55, 15, 63, 111, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:07:30.526128976Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:07:30.526132887Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.526136562Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.526141256Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.526147288Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(5), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:07:30.526176018Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }, filepath = "./demo/sftp/std/testing/out/./512B_random", offset = 0, len = 32768 +[2025-11-18T04:07:30.526215443Z WARN sunset_demo_sftp_std::demosftpserver] Read operation: length + offset > file length. Clipping ( 32768 + 0 > 512) +[2025-11-18T04:07:30.526226241Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 512 +[2025-11-18T04:07:30.526258728Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:07:30.526282496Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.526289660Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.526293808Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.526322569Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.570690246Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:07:30.570758411Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 6, 0, 0, 0, 4, 55, 15, 63, 111, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 126, 0] +[2025-11-18T04:07:30.570779338Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:07:30.570788046Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.570837775Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.570858259Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.570869366Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(6), Read { handle: FileHandle(BinString(len=4)), offset: 512, len: 32256 }) +[2025-11-18T04:07:30.570922039Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }, filepath = "./demo/sftp/std/testing/out/./512B_random", offset = 512, len = 32256 +[2025-11-18T04:07:30.570998398Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF +[2025-11-18T04:07:30.571046304Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.571060160Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.571064617Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.571069197Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.571566825Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:07:30.571598231Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 7, 0, 0, 0, 4, 55, 15, 63, 111] +[2025-11-18T04:07:30.571607269Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:07:30.571611077Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.571614721Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:07:30.571619343Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:07:30.571644779Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(7), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:07:30.571674342Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./512B_random" was successful +[2025-11-18T04:07:30.571691172Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.571714930Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.571722568Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.571726305Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.572166370Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.572196334Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 8, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.572207863Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.572211713Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.572215378Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.572219475Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.572244354Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(8), LStat { file_path: TextString("./demo/sftp/std/testing/out/./16kB_random") }) +[2025-11-18T04:07:30.572273908Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./16kB_random" +[2025-11-18T04:07:30.572315957Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./16kB_random" is Attrs { size: Some(16384), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438849), ext_count: None } +[2025-11-18T04:07:30.572350204Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.572357379Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.572361074Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.572383896Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.572882758Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.572913485Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 9, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.572924705Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.572928544Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.572932209Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.572936326Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.572961597Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(9), Stat { file_path: TextString("./demo/sftp/std/testing/out/./16kB_random") }) +[2025-11-18T04:07:30.572990306Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./16kB_random" +[2025-11-18T04:07:30.573027240Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./16kB_random" is Attrs { size: Some(16384), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438849), ext_count: None } +[2025-11-18T04:07:30.573057524Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.573064997Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.573090289Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.573098112Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.573531692Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:07:30.573561255Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 10, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:07:30.573573361Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:07:30.573577241Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.573580916Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:07:30.573585034Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:07:30.573609934Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(10), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./16kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:07:30.573641103Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./16kB_random", mode = SSH_FXF_READ +[2025-11-18T04:07:30.573668485Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:07:30.573713365Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./16kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }) +[2025-11-18T04:07:30.573744678Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.573751905Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.573755600Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.573778133Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.574301062Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:07:30.574331840Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 11, 0, 0, 0, 4, 204, 179, 134, 195, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:07:30.574341609Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:07:30.574345582Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.574349267Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.574353395Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.574378398Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(11), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:07:30.574407169Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }, filepath = "./demo/sftp/std/testing/out/./16kB_random", offset = 0, len = 32768 +[2025-11-18T04:07:30.574441725Z WARN sunset_demo_sftp_std::demosftpserver] Read operation: length + offset > file length. Clipping ( 32768 + 0 > 16384) +[2025-11-18T04:07:30.574468633Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 16384 +[2025-11-18T04:07:30.620633925Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:07:30.620676819Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.620684745Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.620689017Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.620692846Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.621652731Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:07:30.621688409Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 12, 0, 0, 0, 4, 204, 179, 134, 195, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 64, 0] +[2025-11-18T04:07:30.621699990Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:07:30.621703942Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.621707792Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.621731663Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.621760712Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(12), Read { handle: FileHandle(BinString(len=4)), offset: 16384, len: 16384 }) +[2025-11-18T04:07:30.621790399Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }, filepath = "./demo/sftp/std/testing/out/./16kB_random", offset = 16384, len = 16384 +[2025-11-18T04:07:30.621810307Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF +[2025-11-18T04:07:30.621836051Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.621861816Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.621888745Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.621914201Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.622335882Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:07:30.622365929Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 13, 0, 0, 0, 4, 204, 179, 134, 195] +[2025-11-18T04:07:30.622374833Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:07:30.622378662Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.622382296Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:07:30.622386444Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:07:30.622410665Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(13), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:07:30.622439848Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./16kB_random" was successful +[2025-11-18T04:07:30.622476442Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.622486149Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.622509031Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.622517318Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.622914952Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.622945092Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 14, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.622956673Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.622960523Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.622964177Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.622968335Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.622992855Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(14), LStat { file_path: TextString("./demo/sftp/std/testing/out/./64kB_random") }) +[2025-11-18T04:07:30.623021646Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./64kB_random" +[2025-11-18T04:07:30.623062553Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./64kB_random" is Attrs { size: Some(65536), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438850), ext_count: None } +[2025-11-18T04:07:30.623096893Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.623104160Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.623126127Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.623132911Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.623499520Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.623532913Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 15, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.623544112Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.623547952Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.623571576Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.623598504Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.623625278Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(15), Stat { file_path: TextString("./demo/sftp/std/testing/out/./64kB_random") }) +[2025-11-18T04:07:30.623654831Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./64kB_random" +[2025-11-18T04:07:30.623672289Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./64kB_random" is Attrs { size: Some(65536), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438850), ext_count: None } +[2025-11-18T04:07:30.623683067Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.623707988Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.623715018Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.623719836Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.624113147Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:07:30.624143163Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 16, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:07:30.624154744Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:07:30.624178995Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.624205512Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:07:30.624231473Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:07:30.624260243Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(16), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./64kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:07:30.624274058Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./64kB_random", mode = SSH_FXF_READ +[2025-11-18T04:07:30.624299061Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:07:30.624321872Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./64kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }) +[2025-11-18T04:07:30.624347946Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.624355295Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.624380669Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.624388369Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.624962890Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:07:30.624993740Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 17, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:07:30.625003138Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:07:30.625007019Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.625010684Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.625014811Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.625040237Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(17), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:07:30.625068658Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 0, len = 32768 +[2025-11-18T04:07:30.625104974Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:07:30.676397692Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:07:30.676439834Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.676447544Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.676451755Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.676455543Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.677366008Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 58 bytes +[2025-11-18T04:07:30.677396302Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 18, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 25, 5, 0, 0, 0, 19, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:07:30.677409509Z TRACE sunset_sftp::sftphandler::sftphandler] Received 58 bytes to process +[2025-11-18T04:07:30.677413359Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.677416982Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 58 +[2025-11-18T04:07:30.677421337Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 58 +[2025-11-18T04:07:30.677427328Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(18), Read { handle: FileHandle(BinString(len=4)), offset: 32768, len: 32768 }) +[2025-11-18T04:07:30.677434286Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 32768, len = 32768 +[2025-11-18T04:07:30.677449150Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:07:30.725418534Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:07:30.725464310Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 29 bytes +[2025-11-18T04:07:30.725473574Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 29 +[2025-11-18T04:07:30.725477558Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.725482005Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.725488088Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(19), Read { handle: FileHandle(BinString(len=4)), offset: 65536, len: 32768 }) +[2025-11-18T04:07:30.725516324Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 65536, len = 32768 +[2025-11-18T04:07:30.725556500Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF +[2025-11-18T04:07:30.725565167Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.725569089Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.725572733Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.725579620Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.726220195Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:07:30.726251694Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 20, 0, 0, 0, 4, 195, 17, 206, 102] +[2025-11-18T04:07:30.726260701Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:07:30.726264551Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.726268195Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:07:30.726272703Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:07:30.726277840Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(20), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:07:30.726284839Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./64kB_random" was successful +[2025-11-18T04:07:30.726296965Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.726321732Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.726328927Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.726332695Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.726754056Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.726784927Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 21, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.726797125Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.726800944Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.726804598Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.726808736Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.726813677Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(21), LStat { file_path: TextString("./demo/sftp/std/testing/out/./65kB_random") }) +[2025-11-18T04:07:30.726819792Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./65kB_random" +[2025-11-18T04:07:30.726835911Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./65kB_random" is Attrs { size: Some(66560), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438850), ext_count: None } +[2025-11-18T04:07:30.726866185Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.726873401Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.726877014Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.726880586Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.727295586Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.727327949Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 22, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.727339756Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.727343616Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.727347281Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.727351367Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.727355948Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(22), Stat { file_path: TextString("./demo/sftp/std/testing/out/./65kB_random") }) +[2025-11-18T04:07:30.727361867Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./65kB_random" +[2025-11-18T04:07:30.727374559Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./65kB_random" is Attrs { size: Some(66560), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438850), ext_count: None } +[2025-11-18T04:07:30.727383175Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.727407025Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.727414138Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.727417834Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.727764813Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:07:30.727795509Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 23, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:07:30.727806996Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:07:30.727810815Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.727814500Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:07:30.727818597Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:07:30.727842664Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(23), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./65kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:07:30.727874523Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./65kB_random", mode = SSH_FXF_READ +[2025-11-18T04:07:30.727900906Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:07:30.727944983Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./65kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }) +[2025-11-18T04:07:30.727974310Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.727981587Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.727985272Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.728007435Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.728479142Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:07:30.728509478Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 24, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:07:30.728518732Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:07:30.728522674Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.728526339Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.728530559Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.728535521Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(24), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:07:30.728561914Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 0, len = 32768 +[2025-11-18T04:07:30.728626919Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:07:30.822241569Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:07:30.822286676Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.822294478Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.822298544Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.822302353Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.823271801Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:07:30.823302878Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 25, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0] +[2025-11-18T04:07:30.823314571Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:07:30.823318535Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.823322230Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.823327017Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.823333265Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(25), Read { handle: FileHandle(BinString(len=4)), offset: 32768, len: 32768 }) +[2025-11-18T04:07:30.823361233Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 32768, len = 32768 +[2025-11-18T04:07:30.823399700Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:07:30.868777413Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:07:30.868822489Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.868830405Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.868834574Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.868838310Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel \ No newline at end of file diff --git a/demo/sftp/std/testing/out/get-deadlock-client-killed.log b/demo/sftp/std/testing/out/get-deadlock-client-killed.log new file mode 100644 index 00000000..b737b22f --- /dev/null +++ b/demo/sftp/std/testing/out/get-deadlock-client-killed.log @@ -0,0 +1,55 @@ +Server: + +[2025-11-18T04:08:59.480314646Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:08:59.481255533Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:08:59.481288052Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 18, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0] +[2025-11-18T04:08:59.481298059Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:08:59.481322710Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:08:59.481347629Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:08:59.481375365Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:08:59.481403061Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(18), Read { handle: FileHandle(BinString(len=4)), offset: 32768, len: 32768 }) +[2025-11-18T04:08:59.481434849Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 32768, len = 32768 +[2025-11-18T04:08:59.481473199Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:08:59.530448787Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:08:59.530485451Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:08:59.530493205Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:08:59.530497236Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:08:59.530500918Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:09:04.048434784Z WARN sunset_async::async_sunset] rx complete () +[2025-11-18T04:09:04.048557097Z INFO sunset_demo_common::server] Listening on TCP:22... + +Client: + +sftp> get ./64kB_random +debug3: Looking up ./demo/sftp/std/testing/out/./64kB_random +debug3: Sent message fd 3 T:7 I:14 +debug3: Received stat reply T:105 I:14 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./64kB_random to 64kB_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./64kB_random" to local "64kB_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./64kB_random" +debug3: Sent message fd 3 T:17 I:15 +debug3: Received stat reply T:105 I:15 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./64kB_random" +debug3: Sent remote message SSH2_FXP_OPEN I:16 P:./demo/sftp/std/testing/out/./64kB_random M:0x0001 +64kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug3: Received reply T:103 I:17 R:1 +debug3: Received data 0 -> 32767 +debug3: Request range 32768 -> 65535 (0/2) +debug3: Request range 65536 -> 98303 (1/2) +debug2: channel 0: rcvd adjust 534 +debug3: Received reply T:103 I:18 R:2 +debug3: Received data 32768 -> 65535 +debug3: Finish at 98304 ( 1) +64kB_random 100% 64KB 16.0KB/s 00:04 debug3: send packet: type 1 +debug1: channel 0: free: client-session, nchannels 1 +debug3: channel 0: status: The following connections are open: + #0 client-session (t4 r0 i0/0 o0/0 e[write]/0 fd 4/5/6 sock -1 cc -1 io 0x01/0x02) + +Killed by signal 15. +DOWNLOAD Test Results: +============= +Download PASS: 512B_random +Download PASS: 16kB_random +Download PASS: 64kB_random +Download FAIL: 65kB_random +Cleaning up local files... \ No newline at end of file diff --git a/demo/sftp/std/testing/out/success-get-client.log b/demo/sftp/std/testing/out/success-get-client.log new file mode 100644 index 00000000..6dd21981 --- /dev/null +++ b/demo/sftp/std/testing/out/success-get-client.log @@ -0,0 +1,308 @@ +Testing Multiple GETs... +Generating random data files... +Uploading files to any@192.168.69.2... +Connected to 192.168.69.2. +sftp> put ./512B_random +Uploading ./512B_random to ./demo/sftp/std/testing/out/512B_random +512B_random 100% 512 522.1KB/s 00:00 +sftp> put ./16kB_random +Uploading ./16kB_random to ./demo/sftp/std/testing/out/16kB_random +16kB_random 100% 16KB 230.4KB/s 00:00 +sftp> put ./64kB_random +Uploading ./64kB_random to ./demo/sftp/std/testing/out/64kB_random +64kB_random 100% 64KB 214.0KB/s 00:00 +sftp> put ./65kB_random +Uploading ./65kB_random to ./demo/sftp/std/testing/out/65kB_random +65kB_random 100% 65KB 223.5KB/s 00:00 +sftp> +sftp> bye +client_loop: send disconnect: Broken pipe +UPLOAD Test Results: +============= +Upload PASS: 512B_random +Upload PASS: 16kB_random +Upload PASS: 64kB_random +Upload PASS: 65kB_random +Cleaning up original files... +OpenSSH_8.9p1 Ubuntu-3ubuntu0.13, OpenSSL 3.0.2 15 Mar 2022 +debug1: Reading configuration data /home/jubeor/.ssh/config +debug1: Reading configuration data /etc/ssh/ssh_config +debug1: /etc/ssh/ssh_config line 19: include /etc/ssh/ssh_config.d/*.conf matched no files +debug1: /etc/ssh/ssh_config line 21: Applying options for * +debug2: resolve_canonicalize: hostname 192.168.69.2 is address +debug3: ssh_connect_direct: entering +debug1: Connecting to 192.168.69.2 [192.168.69.2] port 22. +debug3: set_sock_tos: set socket 3 IP_TOS 0x10 +debug1: Connection established. +debug1: identity file /home/jubeor/.ssh/id_rsa type 0 +debug1: identity file /home/jubeor/.ssh/id_rsa-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa_sk type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa_sk-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519 type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519_sk type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519_sk-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_xmss type -1 +debug1: identity file /home/jubeor/.ssh/id_xmss-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_dsa type -1 +debug1: identity file /home/jubeor/.ssh/id_dsa-cert type -1 +debug1: Local version string SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.13 +debug1: Remote protocol version 2.0, remote software version Sunset-1 +debug1: compat_banner: no match: Sunset-1 +debug2: fd 3 setting O_NONBLOCK +debug1: Authenticating to 192.168.69.2:22 as 'any' +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory +debug3: order_hostkeyalgs: no algorithms matched; accept original +debug3: send packet: type 20 +debug1: SSH2_MSG_KEXINIT sent +debug3: receive packet: type 20 +debug1: SSH2_MSG_KEXINIT received +debug2: local client KEXINIT proposal +debug2: KEX algorithms: curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,sntrup761x25519-sha512@openssh.com,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,ext-info-c,kex-strict-c-v00@openssh.com +debug2: host key algorithms: ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,sk-ecdsa-sha2-nistp256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256 +debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com +debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com +debug2: MACs ctos: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 +debug2: MACs stoc: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 +debug2: compression ctos: none,zlib@openssh.com,zlib +debug2: compression stoc: none,zlib@openssh.com,zlib +debug2: languages ctos: +debug2: languages stoc: +debug2: first_kex_follows 0 +debug2: reserved 0 +debug2: peer server KEXINIT proposal +debug2: KEX algorithms: mlkem768x25519-sha256,curve25519-sha256,curve25519-sha256@libssh.org,kex-strict-s-v00@openssh.com,kexguess2@matt.ucc.asn.au +debug2: host key algorithms: ssh-ed25519,rsa-sha2-256 +debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes256-ctr +debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes256-ctr +debug2: MACs ctos: hmac-sha2-256 +debug2: MACs stoc: hmac-sha2-256 +debug2: compression ctos: none +debug2: compression stoc: none +debug2: languages ctos: +debug2: languages stoc: +debug2: first_kex_follows 0 +debug2: reserved 0 +debug3: kex_choose_conf: will use strict KEX ordering +debug1: kex: algorithm: curve25519-sha256 +debug1: kex: host key algorithm: ssh-ed25519 +debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: compression: none +debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: compression: none +debug3: send packet: type 30 +debug1: expecting SSH2_MSG_KEX_ECDH_REPLY +debug3: receive packet: type 31 +debug1: SSH2_MSG_KEX_ECDH_REPLY received +debug1: Server host key: ssh-ed25519 SHA256:624v6xNDiTpLgI28RnlKgSBDHRrekRUjQFY9rgia+fg +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory +Warning: Permanently added '192.168.69.2' (ED25519) to the list of known hosts. +debug3: send packet: type 21 +debug1: ssh_packet_send2_wrapped: resetting send seqnr 3 +debug2: ssh_set_newkeys: mode 1 +debug1: rekey out after 134217728 blocks +debug1: SSH2_MSG_NEWKEYS sent +debug1: expecting SSH2_MSG_NEWKEYS +debug3: receive packet: type 21 +debug1: ssh_packet_read_poll2: resetting read seqnr 3 +debug1: SSH2_MSG_NEWKEYS received +debug2: ssh_set_newkeys: mode 0 +debug1: rekey in after 134217728 blocks +debug1: Will attempt key: /home/jubeor/.ssh/id_rsa RSA SHA256:Wc43s4t4VZ5DkRa7At3RMd9E0u/UH4CV75mWxI0G8A4 +debug1: Will attempt key: /home/jubeor/.ssh/id_ecdsa +debug1: Will attempt key: /home/jubeor/.ssh/id_ecdsa_sk +debug1: Will attempt key: /home/jubeor/.ssh/id_ed25519 +debug1: Will attempt key: /home/jubeor/.ssh/id_ed25519_sk +debug1: Will attempt key: /home/jubeor/.ssh/id_xmss +debug1: Will attempt key: /home/jubeor/.ssh/id_dsa +debug2: pubkey_prepare: done +debug3: send packet: type 5 +debug3: receive packet: type 7 +debug1: SSH2_MSG_EXT_INFO received +debug1: kex_input_ext_info: server-sig-algs= +debug3: receive packet: type 6 +debug2: service_accept: ssh-userauth +debug1: SSH2_MSG_SERVICE_ACCEPT received +debug3: send packet: type 50 +debug3: receive packet: type 52 +Authenticated to 192.168.69.2 ([192.168.69.2]:22) using "none". +debug1: channel 0: new [client-session] +debug3: ssh_session2_open: channel_new: 0 +debug2: channel 0: send open +debug3: send packet: type 90 +debug1: Entering interactive session. +debug1: pledge: network +debug3: receive packet: type 91 +debug2: channel_input_open_confirmation: channel 0: callback start +debug2: fd 3 setting TCP_NODELAY +debug3: set_sock_tos: set socket 3 IP_TOS 0x08 +debug2: client_session2_setup: id 0 +debug1: Sending environment. +debug3: Ignored env SHELL +debug3: Ignored env USER_ZDOTDIR +debug3: Ignored env COLORTERM +debug3: Ignored env WSL2_GUI_APPS_ENABLED +debug3: Ignored env TERM_PROGRAM_VERSION +debug3: Ignored env WSL_DISTRO_NAME +debug3: Ignored env NAME +debug3: Ignored env PWD +debug3: Ignored env NIX_PROFILES +debug3: Ignored env LOGNAME +debug3: Ignored env VSCODE_GIT_ASKPASS_NODE +debug3: Ignored env VSCODE_INJECTION +debug3: Ignored env HOME +debug1: channel 0: setting env LANG = "C.UTF-8" +debug2: channel 0: request env confirm 0 +debug3: send packet: type 98 +debug3: Ignored env WSL_INTEROP +debug3: Ignored env LS_COLORS +debug3: Ignored env WAYLAND_DISPLAY +debug3: Ignored env NIX_SSL_CERT_FILE +debug3: Ignored env GIT_ASKPASS +debug3: Ignored env VSCODE_GIT_ASKPASS_EXTRA_ARGS +debug3: Ignored env VSCODE_PYTHON_AUTOACTIVATE_GUARD +debug3: Ignored env TERM +debug3: Ignored env ZDOTDIR +debug3: Ignored env USER +debug3: Ignored env VSCODE_GIT_IPC_HANDLE +debug3: Ignored env DISPLAY +debug3: Ignored env SHLVL +debug3: Ignored env XDG_RUNTIME_DIR +debug3: Ignored env WSLENV +debug3: Ignored env VSCODE_GIT_ASKPASS_MAIN +debug3: Ignored env XDG_DATA_DIRS +debug3: Ignored env PATH +debug3: Ignored env DBUS_SESSION_BUS_ADDRESS +debug3: Ignored env HOSTTYPE +debug3: Ignored env PULSE_SERVER +debug3: Ignored env OLDPWD +debug3: Ignored env TERM_PROGRAM +debug3: Ignored env VSCODE_IPC_HOOK_CLI +debug3: Ignored env _ +debug1: Sending subsystem: sftp +debug2: channel 0: request subsystem confirm 1 +debug3: send packet: type 98 +debug2: channel_input_open_confirmation: channel 0: callback done +debug2: channel 0: open confirm rwindow 1000 rmax 1000 +debug3: receive packet: type 99 +debug2: channel_input_status_confirm: type 99 id 0 +debug2: subsystem request accepted on channel 0 +debug2: Remote version: 3 +Connected to 192.168.69.2. +debug2: Sending SSH2_FXP_REALPATH "." +debug3: Sent message fd 3 T:16 I:1 +debug3: SSH2_FXP_REALPATH . -> ./demo/sftp/std/testing/out/ +sftp> get ./512B_random +debug3: Looking up ./demo/sftp/std/testing/out/./512B_random +debug3: Sent message fd 3 T:7 I:2 +debug3: Received stat reply T:105 I:2 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./512B_random to 512B_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./512B_random" to local "512B_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./512B_random" +debug3: Sent message fd 3 T:17 I:3 +debug3: Received stat reply T:105 I:3 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./512B_random" +debug3: Sent remote message SSH2_FXP_OPEN I:4 P:./demo/sftp/std/testing/out/./512B_random M:0x0001 +512B_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug3: Received reply T:103 I:5 R:1 +debug3: Received data 0 -> 511 +debug3: Short data block, re-requesting 512 -> 32767 ( 1) +debug3: Finish at 32768 ( 1) +debug3: Received reply T:101 I:6 R:1 +512B_random 100% 512 10.8KB/s 00:00 +debug3: Sent message SSH2_FXP_CLOSE I:7 +debug3: SSH2_FXP_STATUS 0 +sftp> get ./16kB_random +debug3: Looking up ./demo/sftp/std/testing/out/./16kB_random +debug3: Sent message fd 3 T:7 I:8 +debug3: Received stat reply T:105 I:8 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./16kB_random to 16kB_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./16kB_random" to local "16kB_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./16kB_random" +debug3: Sent message fd 3 T:17 I:9 +debug2: channel 0: rcvd adjust 516 +debug3: Received stat reply T:105 I:9 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./16kB_random" +debug3: Sent remote message SSH2_FXP_OPEN I:10 P:./demo/sftp/std/testing/out/./16kB_random M:0x0001 +16kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug3: Received reply T:103 I:11 R:1 +debug3: Received data 0 -> 16383 +debug3: Short data block, re-requesting 16384 -> 32767 ( 1) +debug3: Finish at 32768 ( 1) +debug3: Received reply T:101 I:12 R:1 +16kB_random 100% 16KB 333.6KB/s 00:00 +debug3: Sent message SSH2_FXP_CLOSE I:13 +debug3: SSH2_FXP_STATUS 0 +sftp> get ./64kB_random +debug3: Looking up ./demo/sftp/std/testing/out/./64kB_random +debug3: Sent message fd 3 T:7 I:14 +debug3: Received stat reply T:105 I:14 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./64kB_random to 64kB_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./64kB_random" to local "64kB_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./64kB_random" +debug3: Sent message fd 3 T:17 I:15 +debug3: Received stat reply T:105 I:15 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./64kB_random" +debug3: Sent remote message SSH2_FXP_OPEN I:16 P:./demo/sftp/std/testing/out/./64kB_random M:0x0001 +64kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug3: Received reply T:103 I:17 R:1 +debug3: Received data 0 -> 32767 +debug3: Request range 32768 -> 65535 (0/2) +debug3: Request range 65536 -> 98303 (1/2) +debug2: channel 0: rcvd adjust 520 +debug3: Received reply T:103 I:18 R:2 +debug3: Received data 32768 -> 65535 +debug3: Finish at 98304 ( 1) +debug3: Received reply T:101 I:19 R:1 +64kB_random 100% 64KB 623.4KB/s 00:00 +debug3: Sent message SSH2_FXP_CLOSE I:20 +debug3: SSH2_FXP_STATUS 0 +sftp> get ./65kB_random +debug3: Looking up ./demo/sftp/std/testing/out/./65kB_random +debug3: Sent message fd 3 T:7 I:21 +debug3: Received stat reply T:105 I:21 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./65kB_random to 65kB_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./65kB_random" to local "65kB_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./65kB_random" +debug3: Sent message fd 3 T:17 I:22 +debug3: Received stat reply T:105 I:22 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./65kB_random" +debug3: Sent remote message SSH2_FXP_OPEN I:23 P:./demo/sftp/std/testing/out/./65kB_random M:0x0001 +65kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug2: channel 0: window 1997312 sent adjust 99840 +debug3: Received reply T:103 I:24 R:1 +debug3: Received data 0 -> 32767 +debug3: Request range 32768 -> 65535 (0/2) +debug3: Request range 65536 -> 98303 (1/2) +debug3: Received reply T:103 I:25 R:2 +debug3: Received data 32768 -> 65535 +debug3: Finish at 98304 ( 1) +debug3: Received reply T:103 I:26 R:1 +debug3: Received data 65536 -> 66559 +debug3: Short data block, re-requesting 66560 -> 98303 ( 1) +debug3: Finish at 98304 ( 1) +debug3: Received reply T:101 I:27 R:1 +65kB_random 100% 65KB 87.8KB/s 00:00 +debug3: Sent message SSH2_FXP_CLOSE I:28 +debug3: SSH2_FXP_STATUS 0 +sftp> +sftp> bye +debug2: channel 0: read failed rfd 4 maxlen 946: Broken pipe +debug2: channel 0: read failed +debug2: chan_shutdown_read: channel 0: (i0 o0 sock -1 wfd 4 efd 6 [write]) +debug2: channel 0: input open -> drain +debug2: channel 0: ibuf empty +debug2: channel 0: send eof +debug3: send packet: type 96 +debug2: channel 0: input drain -> closed +debug3: send packet: type 1 +client_loop: send disconnect: Broken pipe +DOWNLOAD Test Results: +============= +Download PASS: 512B_random +Download PASS: 16kB_random +Download PASS: 64kB_random +Download PASS: 65kB_random +Cleaning up local files... \ No newline at end of file diff --git a/demo/sftp/std/testing/out/success-get-server.log b/demo/sftp/std/testing/out/success-get-server.log new file mode 100644 index 00000000..a78cdebb --- /dev/null +++ b/demo/sftp/std/testing/out/success-get-server.log @@ -0,0 +1,351 @@ +[2025-11-18T04:00:55.665183905Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.665245066Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(2), LStat { file_path: TextString("./demo/sftp/std/testing/out/./512B_random") }) +[2025-11-18T04:00:55.665306824Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./512B_random" +[2025-11-18T04:00:55.665390942Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./512B_random" is Attrs { size: Some(512), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438454), ext_count: None } +[2025-11-18T04:00:55.665420750Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.665429106Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.665497840Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.665556398Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.666039369Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:00:55.666069600Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 3, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:00:55.666080764Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:00:55.666084654Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.666088265Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:00:55.666092392Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.666118188Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(3), Stat { file_path: TextString("./demo/sftp/std/testing/out/./512B_random") }) +[2025-11-18T04:00:55.666146165Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./512B_random" +[2025-11-18T04:00:55.666184638Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./512B_random" is Attrs { size: Some(512), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438454), ext_count: None } +[2025-11-18T04:00:55.666220106Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.666227360Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.666231188Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.666234727Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.666614742Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:00:55.666676839Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 4, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:00:55.666693899Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:00:55.666721753Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.666728760Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:00:55.666734903Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:00:55.666740110Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(4), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./512B_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:00:55.666752653Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./512B_random", mode = SSH_FXF_READ +[2025-11-18T04:00:55.666757016Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:00:55.666781248Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./512B_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }) +[2025-11-18T04:00:55.666806961Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.666833560Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.666859870Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.666886644Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.667464094Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:00:55.667505150Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 5, 0, 0, 0, 4, 55, 15, 63, 111, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:00:55.667515666Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:00:55.667519565Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.667523198Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:55.667527684Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:55.667556248Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(5), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:00:55.667586365Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }, filepath = "./demo/sftp/std/testing/out/./512B_random", offset = 0, len = 32768 +[2025-11-18T04:00:55.667608467Z WARN sunset_demo_sftp_std::demosftpserver] Read operation: length + offset > file length. Clipping ( 32768 + 0 > 512) +[2025-11-18T04:00:55.667633306Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 512 +[2025-11-18T04:00:55.667666531Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:00:55.667692091Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.667700600Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.667704438Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.667729555Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.713066511Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:00:55.713107103Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 6, 0, 0, 0, 4, 55, 15, 63, 111, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 126, 0] +[2025-11-18T04:00:55.713116827Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:00:55.713120696Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.713124369Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:55.713129041Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:55.713134649Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(6), Read { handle: FileHandle(BinString(len=4)), offset: 512, len: 32256 }) +[2025-11-18T04:00:55.713142644Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }, filepath = "./demo/sftp/std/testing/out/./512B_random", offset = 512, len = 32256 +[2025-11-18T04:00:55.713156751Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF +[2025-11-18T04:00:55.713163511Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.713189492Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.713196540Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.713200728Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.713610407Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:00:55.713647800Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 7, 0, 0, 0, 4, 55, 15, 63, 111] +[2025-11-18T04:00:55.713656525Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:00:55.713660343Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.713663954Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:00:55.713668225Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:00:55.713672875Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(7), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:00:55.713678710Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./512B_random" was successful +[2025-11-18T04:00:55.713691067Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.713694741Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.713698280Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.713701769Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.714137882Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:00:55.714168771Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 8, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:00:55.714181489Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:00:55.714185317Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.714188939Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:00:55.714193034Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.714216906Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(8), LStat { file_path: TextString("./demo/sftp/std/testing/out/./16kB_random") }) +[2025-11-18T04:00:55.714246807Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./16kB_random" +[2025-11-18T04:00:55.714286124Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./16kB_random" is Attrs { size: Some(16384), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438454), ext_count: None } +[2025-11-18T04:00:55.714319174Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.714326356Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.714349487Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.714374768Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.714840299Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:00:55.714870345Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 9, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:00:55.714881622Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:00:55.714885419Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.714889072Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:00:55.714893177Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.714918891Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(9), Stat { file_path: TextString("./demo/sftp/std/testing/out/./16kB_random") }) +[2025-11-18T04:00:55.714946879Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./16kB_random" +[2025-11-18T04:00:55.714985578Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./16kB_random" is Attrs { size: Some(16384), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438454), ext_count: None } +[2025-11-18T04:00:55.715021828Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.715028969Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.715032828Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.715036357Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.715466142Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:00:55.715495179Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 10, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:00:55.715506652Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:00:55.715510500Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.715514122Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:00:55.715518218Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:00:55.715543684Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(10), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./16kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:00:55.715573792Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./16kB_random", mode = SSH_FXF_READ +[2025-11-18T04:00:55.715601357Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:00:55.715645088Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./16kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }) +[2025-11-18T04:00:55.715675926Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.715683221Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.715706260Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.715731798Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.716266753Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:00:55.716297673Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 11, 0, 0, 0, 4, 204, 179, 134, 195, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:00:55.716307592Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:00:55.716311482Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.716315124Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:55.716338348Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:55.716366994Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(11), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:00:55.716379578Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }, filepath = "./demo/sftp/std/testing/out/./16kB_random", offset = 0, len = 32768 +[2025-11-18T04:00:55.716412001Z WARN sunset_demo_sftp_std::demosftpserver] Read operation: length + offset > file length. Clipping ( 32768 + 0 > 16384) +[2025-11-18T04:00:55.716438867Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 16384 +[2025-11-18T04:00:55.762253804Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:00:55.762324452Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.762334053Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.762339033Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.762343859Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.763463786Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:00:55.763502124Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 12, 0, 0, 0, 4, 204, 179, 134, 195, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 64, 0] +[2025-11-18T04:00:55.763512085Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:00:55.763515943Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.763519689Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:55.763524350Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:55.763551062Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(12), Read { handle: FileHandle(BinString(len=4)), offset: 16384, len: 16384 }) +[2025-11-18T04:00:55.763582548Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }, filepath = "./demo/sftp/std/testing/out/./16kB_random", offset = 16384, len = 16384 +[2025-11-18T04:00:55.763619096Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF +[2025-11-18T04:00:55.763630240Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.763652990Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.763678303Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.763705405Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.764155975Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:00:55.764185908Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 13, 0, 0, 0, 4, 204, 179, 134, 195] +[2025-11-18T04:00:55.764194664Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:00:55.764198554Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.764202165Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:00:55.764206333Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:00:55.764231398Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(13), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:00:55.764260054Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./16kB_random" was successful +[2025-11-18T04:00:55.764296274Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.764304793Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.764308498Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.764331608Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.764773803Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:00:55.764804712Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 14, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:00:55.764816370Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:00:55.764820229Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.764823841Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:00:55.764827926Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.764852549Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(14), LStat { file_path: TextString("./demo/sftp/std/testing/out/./64kB_random") }) +[2025-11-18T04:00:55.764883345Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./64kB_random" +[2025-11-18T04:00:55.764924143Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./64kB_random" is Attrs { size: Some(65536), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438455), ext_count: None } +[2025-11-18T04:00:55.764963604Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.764971959Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.764995049Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.765003425Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.765431183Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:00:55.765462833Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 15, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:00:55.765475027Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:00:55.765479616Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.765483248Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:00:55.765508077Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.765518377Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(15), Stat { file_path: TextString("./demo/sftp/std/testing/out/./64kB_random") }) +[2025-11-18T04:00:55.765524664Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./64kB_random" +[2025-11-18T04:00:55.765539511Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./64kB_random" is Attrs { size: Some(65536), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438455), ext_count: None } +[2025-11-18T04:00:55.765567746Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.765574887Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.765578529Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.765584693Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.766011422Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:00:55.766042939Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 16, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:00:55.766054566Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:00:55.766058425Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.766062047Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:00:55.766066132Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:00:55.766090518Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(16), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./64kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:00:55.766123105Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./64kB_random", mode = SSH_FXF_READ +[2025-11-18T04:00:55.766149220Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:00:55.766195904Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./64kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }) +[2025-11-18T04:00:55.766227102Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.766235097Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.766238842Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.766260810Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.766789201Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:00:55.766822477Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 17, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:00:55.766831871Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:00:55.766835771Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.766839445Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:55.766843509Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:55.766849148Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(17), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:00:55.766857575Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 0, len = 32768 +[2025-11-18T04:00:55.766872742Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:00:55.818563436Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:00:55.818607218Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.818615470Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.818619627Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.818623455Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.819563191Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 58 bytes +[2025-11-18T04:00:55.819598515Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 18, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 25, 5, 0, 0, 0, 19, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:00:55.819610441Z TRACE sunset_sftp::sftphandler::sftphandler] Received 58 bytes to process +[2025-11-18T04:00:55.819614320Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.819618065Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 58 +[2025-11-18T04:00:55.819622459Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 58 +[2025-11-18T04:00:55.819628046Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(18), Read { handle: FileHandle(BinString(len=4)), offset: 32768, len: 32768 }) +[2025-11-18T04:00:55.819634909Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 32768, len = 32768 +[2025-11-18T04:00:55.819649798Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:00:55.868682151Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:00:55.868726129Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 29 bytes +[2025-11-18T04:00:55.868734330Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 29 +[2025-11-18T04:00:55.868738569Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:55.868743220Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:55.868749167Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(19), Read { handle: FileHandle(BinString(len=4)), offset: 65536, len: 32768 }) +[2025-11-18T04:00:55.868756535Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 65536, len = 32768 +[2025-11-18T04:00:55.868770940Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF +[2025-11-18T04:00:55.868777412Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.868781096Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.868784635Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.868788329Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.869538429Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:00:55.869571994Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 20, 0, 0, 0, 4, 195, 17, 206, 102] +[2025-11-18T04:00:55.869581419Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:00:55.869585236Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.869588920Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:00:55.869593468Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:00:55.869598643Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(20), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:00:55.869605013Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./64kB_random" was successful +[2025-11-18T04:00:55.869617021Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.869620848Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.869624378Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.869627928Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.870109675Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:00:55.870147304Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 21, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:00:55.870158911Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:00:55.870162780Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.870166381Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:00:55.870170487Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.870174983Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(21), LStat { file_path: TextString("./demo/sftp/std/testing/out/./65kB_random") }) +[2025-11-18T04:00:55.870181579Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./65kB_random" +[2025-11-18T04:00:55.870202528Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./65kB_random" is Attrs { size: Some(66560), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438455), ext_count: None } +[2025-11-18T04:00:55.870216543Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.870220267Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.870223797Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.870227316Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.870631336Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:00:55.870661073Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 22, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:00:55.870672957Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:00:55.870718725Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.870723098Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:00:55.870727224Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.870752393Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(22), Stat { file_path: TextString("./demo/sftp/std/testing/out/./65kB_random") }) +[2025-11-18T04:00:55.870781152Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./65kB_random" +[2025-11-18T04:00:55.870823699Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./65kB_random" is Attrs { size: Some(66560), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438455), ext_count: None } +[2025-11-18T04:00:55.870854805Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.870861977Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.870884809Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.870909864Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.871287348Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:00:55.871316292Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 23, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:00:55.871327775Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:00:55.871331624Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.871335235Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:00:55.871358994Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:00:55.871386025Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(23), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./65kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:00:55.871401984Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./65kB_random", mode = SSH_FXF_READ +[2025-11-18T04:00:55.871407159Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:00:55.871429652Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./65kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }) +[2025-11-18T04:00:55.871437802Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.871441475Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.871465748Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.871472755Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.872012803Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:00:55.872042880Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 24, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:00:55.872051955Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:00:55.872055834Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.872059467Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:55.872063572Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:55.872068357Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(24), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:00:55.872093875Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 0, len = 32768 +[2025-11-18T04:00:55.872129796Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:00:56.048175463Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:00:56.048246934Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:56.048263223Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:56.048272051Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:56.048280180Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:56.128752382Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 58 bytes +[2025-11-18T04:00:56.128827475Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 25, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 25, 5, 0, 0, 0, 26, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:00:56.128947400Z TRACE sunset_sftp::sftphandler::sftphandler] Received 58 bytes to process +[2025-11-18T04:00:56.129007450Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:56.129023039Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 58 +[2025-11-18T04:00:56.129073612Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 58 +[2025-11-18T04:00:56.129134588Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(25), Read { handle: FileHandle(BinString(len=4)), offset: 32768, len: 32768 }) +[2025-11-18T04:00:56.129197118Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 32768, len = 32768 +[2025-11-18T04:00:56.129281842Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:00:56.571741252Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:00:56.571788635Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 29 bytes +[2025-11-18T04:00:56.571796620Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 29 +[2025-11-18T04:00:56.571800725Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:56.571805592Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:56.571836934Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(26), Read { handle: FileHandle(BinString(len=4)), offset: 65536, len: 32768 }) +[2025-11-18T04:00:56.571867299Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 65536, len = 32768 +[2025-11-18T04:00:56.571887909Z WARN sunset_demo_sftp_std::demosftpserver] Read operation: length + offset > file length. Clipping ( 32768 + 65536 > 66560) +[2025-11-18T04:00:56.571912048Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 1024 +[2025-11-18T04:00:56.571973714Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:00:56.571998913Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:56.572006229Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:56.572010056Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:56.572013740Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:56.612043057Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:00:56.612080511Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 27, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 1, 4, 0, 0, 0, 124, 0] +[2025-11-18T04:00:56.612090348Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:00:56.612094268Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:56.612097901Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:56.612102737Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:56.612129695Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(27), Read { handle: FileHandle(BinString(len=4)), offset: 66560, len: 31744 }) +[2025-11-18T04:00:56.612159638Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 66560, len = 31744 +[2025-11-18T04:00:56.612179991Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF +[2025-11-18T04:00:56.612188727Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:56.612192482Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:56.612213977Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:56.612221015Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:56.612667861Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:00:56.612702804Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 28, 0, 0, 0, 4, 220, 94, 228, 181] +[2025-11-18T04:00:56.612711519Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:00:56.612715368Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:56.612719288Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:00:56.612723435Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:00:56.612750280Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(28), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:00:56.612779163Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./65kB_random" was successful +[2025-11-18T04:00:56.612796758Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:56.612819138Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:56.612828162Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:56.612832453Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:56.613342157Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 0 bytes +[2025-11-18T04:00:56.613381792Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [] +[2025-11-18T04:00:56.613390528Z DEBUG sunset_sftp::sftphandler::sftphandler] client disconnected +[2025-11-18T04:00:56.613394932Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing returned: Err(ClientDisconnected) +[2025-11-18T04:00:56.613402269Z WARN sunset_demo_sftp_std] sftp_loop finished: Err(ChannelEOF) +[2025-11-18T04:00:56.613409008Z WARN sunset_demo_common::server] Ended with error ChannelEOF +[2025-11-18T04:00:56.613464932Z INFO sunset_demo_common::server] Listening on TCP:22... \ No newline at end of file diff --git a/demo/sftp/std/testing/test_get.sh b/demo/sftp/std/testing/test_get.sh index e498fee9..fa2f4c03 100755 --- a/demo/sftp/std/testing/test_get.sh +++ b/demo/sftp/std/testing/test_get.sh @@ -1,6 +1,6 @@ #!/bin/bash -echo "Testing Stats..." +echo "Testing Multiple GETs..." # Set remote server details REMOTE_HOST="192.168.69.2" @@ -42,7 +42,7 @@ echo "Cleaning up original files..." rm -f -r ./*_random # Download the files -sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +sftp -vvvvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'get ./%s\n' "${FILES[@]}") bye diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index bbf2c3ec..22855b5b 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -503,6 +503,7 @@ where let processing_loop = async { loop { + trace!("SFTP: About to read bytes from SSH Channel"); let lr = chan_in.read(buffer_in).await?; debug!("SFTP <---- received: {:?} bytes", lr); From 9dd75704543229b404c70abaa27e62c8ef8cd17a Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 19 Nov 2025 11:27:57 +1100 Subject: [PATCH 187/393] [skip ci] small changes to make visible SftpOutputPipe total bytes communicated There is no data lost in these communications --- demo/sftp/std/src/main.rs | 5 +++-- sftp/src/sftphandler/sftpoutputchannelhandler.rs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 4f65f975..1ba35e54 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -213,8 +213,10 @@ async fn main(spawner: Spawner) { .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) .filter_module( "sunset_sftp::sftphandler::sftpoutputchannelhandler", - log::LevelFilter::Info, + log::LevelFilter::Debug, ) + // .filter_module("sunset::channel", log::LevelFilter::Trace) + // .filter_module("sunset_async::async_sunset", log::LevelFilter::Trace) // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) // .filter_module("sunset_sftp::sftpserver", log::LevelFilter::Info) @@ -222,7 +224,6 @@ async fn main(spawner: Spawner) { // .filter_module("sunset::encrypt", log::LevelFilter::Info) // .filter_module("sunset::conn", log::LevelFilter::Info) // .filter_module("sunset::kex", log::LevelFilter::Info) - // .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) // .filter_module("async_io", log::LevelFilter::Info) // .filter_module("polling", log::LevelFilter::Info) // .filter_module("embassy_net", log::LevelFilter::Info) diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 85d051f8..29b611fe 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -87,7 +87,7 @@ impl<'a, const N: usize> SftpOutputConsumer<'a, N> { _total = *lock; } - debug!("Output Consumer: Reads {rl} bytes. Total {_total}"); + debug!("Output Consumer: ---> Reads {rl} bytes. Total {_total}"); if rl > 0 { self.ssh_chan_out.write_all(&buf[..rl]).await?; debug!("Output Consumer: Written {:?} bytes ", &buf[..rl].len()); @@ -156,7 +156,7 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { _total = *lock; } - debug!("Output Producer: Sends {:?} bytes. Total {_total}", buf.len()); + debug!("Output Producer: <--- Sends {:?} bytes. Total {_total}", buf.len()); trace!("Output Producer: Sending buffer {:?}", buf); // writer.write_all(buf); // ??? error[E0596]: cannot borrow `*writer` as mutable, as it is behind a `&` reference From fe54d2bebb0d13125d8a5c986545f3e00c4af9ca Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 19 Nov 2025 11:32:39 +1100 Subject: [PATCH 188/393] [skip ci] Removing outdated check in SftpHandler.process The fragmented packet handling will take care of that situation --- sftp/src/sftphandler/sftphandler.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 22855b5b..f56e9dae 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -128,11 +128,12 @@ where trace!("Received {:} bytes to process", buf.len()); - if !matches!(self.state, SftpHandleState::Fragmented(_)) - & buf.len().lt(&SFTP_MINIMUM_PACKET_LEN) - { - return Err(WireError::PacketWrong.into()); - } + // TODO: Should go out. fragmented packet handling should take care of it + // if !matches!(self.state, SftpHandleState::Fragmented(_)) + // & buf.len().lt(&SFTP_MINIMUM_PACKET_LEN) + // { + // return Err(WireError::PacketWrong.into()); + // } trace!("Entering loop to process the full received buffer"); while buf.len() > 0 { From 0318c632546b09a3c0a9ed090175b57d356ad3b1 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 19 Nov 2025 13:37:47 +1100 Subject: [PATCH 189/393] [skip ci] Removed nested enum in Sftp process FSM --- sftp/src/sftphandler/sftphandler.rs | 422 +++++++++++++--------------- 1 file changed, 189 insertions(+), 233 deletions(-) diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index f56e9dae..38de0b93 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -3,8 +3,8 @@ use super::PartialWriteRequestTracker; use crate::error::SftpError; use crate::handles::OpaqueFileHandle; use crate::proto::{ - self, InitVersionLowest, LStat, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, - SftpNum, SftpPacket, Stat, StatusCode, + self, InitVersionLowest, LStat, ReqId, SFTP_VERSION, SftpNum, SftpPacket, Stat, + StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; use crate::server::{DirReply, ReadReply}; @@ -33,13 +33,8 @@ enum SftpHandleState { Initializing, /// The handle is ready to process requests Idle, - /// The request is fragmented and needs special handling - Fragmented(FragmentedRequestState), -} - -/// FSM subset to handle fragmented request as part of [`SftpHandleState`] -#[derive(Debug, PartialEq, Eq)] -enum FragmentedRequestState { + // /// The request is fragmented and needs special handling + // Fragmented(FragmentedRequestState), /// A request that is clipped needs to be process. It cannot be decoded /// and more bytes are needed ProcessingClippedRequest, @@ -128,13 +123,6 @@ where trace!("Received {:} bytes to process", buf.len()); - // TODO: Should go out. fragmented packet handling should take care of it - // if !matches!(self.state, SftpHandleState::Fragmented(_)) - // & buf.len().lt(&SFTP_MINIMUM_PACKET_LEN) - // { - // return Err(WireError::PacketWrong.into()); - // } - trace!("Entering loop to process the full received buffer"); while buf.len() > 0 { debug!( @@ -144,210 +132,202 @@ where ); match &self.state { - // There is a fragmented request in process of processing - SftpHandleState::Fragmented(fragment_case) => match fragment_case { - FragmentedRequestState::ProcessingClippedRequest => { - if let Err(e) = self - .incomplete_request_holder - .try_append_for_valid_request(&buf) - { - match e { - RequestHolderError::RanOut => { - warn!( - "There was not enough bytes in the buffer_in. \ + SftpHandleState::ProcessingClippedRequest => { + if let Err(e) = self + .incomplete_request_holder + .try_append_for_valid_request(&buf) + { + match e { + RequestHolderError::RanOut => { + warn!( + "There was not enough bytes in the buffer_in. \ We will continue adding bytes" - ); - buf = &buf[self - .incomplete_request_holder - .appended()..]; - continue; - } - RequestHolderError::WireError(WireError::RanOut) => { - warn!( - "WIRE ERROR: There was not enough bytes in the buffer_in. \ + ); + buf = &buf + [self.incomplete_request_holder.appended()..]; + continue; + } + RequestHolderError::WireError(WireError::RanOut) => { + warn!( + "WIRE ERROR: There was not enough bytes in the buffer_in. \ We will continue adding bytes" - ); - buf = &buf[self - .incomplete_request_holder - .appended()..]; - continue; - } - RequestHolderError::NoRoom => { - warn!( - "The request holder is full but the request in is incomplete. \ + ); + buf = &buf + [self.incomplete_request_holder.appended()..]; + continue; + } + RequestHolderError::NoRoom => { + warn!( + "The request holder is full but the request in is incomplete. \ We will try to decode it" - ); - } + ); + } - _ => { - error!( - "Unhandled error completing incomplete request {:?}", - e, - ); - return Err(SunsetError::Bug.into()); - } + _ => { + error!( + "Unhandled error completing incomplete request {:?}", + e, + ); + return Err(SunsetError::Bug.into()); } - } else { - debug!( - "Incomplete request holder completed the request!" - ); } + } else { + debug!("Incomplete request holder completed the request!"); + } - let used = self.incomplete_request_holder.appended(); - buf = &buf[used..]; + let used = self.incomplete_request_holder.appended(); + buf = &buf[used..]; - let mut source = SftpSource::new( - &self.incomplete_request_holder.try_get_ref()?, - ); - trace!("Internal Source Content: {:?}", source); + let mut source = SftpSource::new( + &self.incomplete_request_holder.try_get_ref()?, + ); + trace!("Internal Source Content: {:?}", source); - match SftpPacket::decode_request(&mut source) { - Ok(request) => { - Self::handle_general_request( - &mut self.file_server, - output_producer, - request, - ) - .await?; - self.incomplete_request_holder.reset(); - self.state = SftpHandleState::Idle; - } - Err(e) => match e { - WireError::UnknownPacket { number } => { - warn!( - "Unknown packet: packetId = {:?}. Will flush \ + match SftpPacket::decode_request(&mut source) { + Ok(request) => { + Self::handle_general_request( + &mut self.file_server, + output_producer, + request, + ) + .await?; + self.incomplete_request_holder.reset(); + self.state = SftpHandleState::Idle; + } + Err(e) => match e { + WireError::UnknownPacket { number } => { + warn!( + "Unknown packet: packetId = {:?}. Will flush \ its length and send unsupported back", - number - ); + number + ); - let req_id = ReqId(source.peak_packet_req_id()?); - let len = - source.peak_total_packet_len()? as usize; - source.consume_first(len)?; - output_producer - .send_status( - req_id, - StatusCode::SSH_FX_OP_UNSUPPORTED, - "Error decoding SFTP Packet", - ) - .await?; + let req_id = ReqId(source.peak_packet_req_id()?); + let len = source.peak_total_packet_len()? as usize; + source.consume_first(len)?; + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_OP_UNSUPPORTED, + "Error decoding SFTP Packet", + ) + .await?; + } + WireError::RanOut => match Self::handle_ran_out( + &mut self.file_server, + output_producer, + &mut source, + ) + .await + { + Ok(holder) => { + self.partial_write_request_tracker = + Some(holder); + self.incomplete_request_holder.reset(); + self.state = + SftpHandleState::ProcessingLongRequest; } - WireError::RanOut => match Self::handle_ran_out( - &mut self.file_server, - output_producer, - &mut source, - ) - .await - { - Ok(holder) => { - self.partial_write_request_tracker = - Some(holder); - self.incomplete_request_holder.reset(); - self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); + Err(e) => match e { + _ => { + error!( + "handle_ran_out finished with error: {:?}", + e + ); + return Err(SunsetError::Bug.into()); } - Err(e) => match e { - _ => { - error!( - "handle_ran_out finished with error: {:?}", - e - ); - return Err(SunsetError::Bug.into()); - } - }, }, - WireError::NoRoom => { - error!("Not enough space to fit the request") - } - _ => { - error!( - "Unhandled error decoding assembled packet: {:?}", - e - ); - return Err(WireError::PacketWrong.into()); - } }, - } + WireError::NoRoom => { + error!("Not enough space to fit the request") + } + _ => { + error!( + "Unhandled error decoding assembled packet: {:?}", + e + ); + return Err(WireError::PacketWrong.into()); + } + }, } - FragmentedRequestState::ProcessingLongRequest => { - let mut source = SftpSource::new(&buf); - trace!("Source content: {:?}", source); - - let mut write_tracker = if let Some(wt) = - self.partial_write_request_tracker.take() - { - wt - } else { - error!( - "BUG: FragmentedRequestState::ProcessingLongRequest cannot take the write tracker" - ); - return Err(SunsetError::Bug.into()); - }; + } + SftpHandleState::ProcessingLongRequest => { + let mut source = SftpSource::new(&buf); + trace!("Source content: {:?}", source); + + let mut write_tracker = if let Some(wt) = + self.partial_write_request_tracker.take() + { + wt + } else { + error!( + "BUG: SftpHandleState::ProcessingLongRequest cannot take the write tracker" + ); + return Err(SunsetError::Bug.into()); + }; - let opaque_handle = write_tracker.get_opaque_file_handle(); + let opaque_handle = write_tracker.get_opaque_file_handle(); - let usable_data = source - .remaining() - .min(write_tracker.get_remain_data_len() as usize); + let usable_data = source + .remaining() + .min(write_tracker.get_remain_data_len() as usize); - let data_segment = source.dec_as_binstring(usable_data)?; + let data_segment = source.dec_as_binstring(usable_data)?; - let data_segment_len = u32::try_from(data_segment.0.len()) - .map_err(|e| { + let data_segment_len = u32::try_from(data_segment.0.len()) + .map_err(|e| { error!("Error casting data segment len to u32: {e}"); SunsetError::Bug })?; - let current_write_offset = - write_tracker.get_remain_data_offset(); - write_tracker - .update_remaining_after_partial_write(data_segment_len); + let current_write_offset = + write_tracker.get_remain_data_offset(); + write_tracker + .update_remaining_after_partial_write(data_segment_len); - debug!( - "Processing successive chunks of a long write packet. \ + debug!( + "Processing successive chunks of a long write packet. \ Writing : opaque_handle = {:?}, write_offset = {:?}, \ data_segment = {:?}, data remaining = {:?}", - opaque_handle, - current_write_offset, - data_segment, - write_tracker.get_remain_data_len() - ); - - match self.file_server.write( - &opaque_handle, - current_write_offset, - data_segment.as_ref(), - ) { - Ok(_) => { - if write_tracker.get_remain_data_len() > 0 { - self.partial_write_request_tracker = - Some(write_tracker); - } else { - output_producer - .send_status( - write_tracker.get_req_id(), - StatusCode::SSH_FX_OK, - "", - ) - .await?; - info!("Finished multi part Write Request"); - self.state = SftpHandleState::Idle; - } - } - Err(e) => { - error!("SFTP write thrown: {:?}", e); + opaque_handle, + current_write_offset, + data_segment, + write_tracker.get_remain_data_len() + ); + + match self.file_server.write( + &opaque_handle, + current_write_offset, + data_segment.as_ref(), + ) { + Ok(_) => { + if write_tracker.get_remain_data_len() > 0 { + self.partial_write_request_tracker = + Some(write_tracker); + } else { output_producer .send_status( write_tracker.get_req_id(), - StatusCode::SSH_FX_FAILURE, - "error writing", + StatusCode::SSH_FX_OK, + "", ) .await?; + info!("Finished multi part Write Request"); self.state = SftpHandleState::Idle; } - }; - buf = &buf[buf.len() - source.remaining()..]; - } - }, - + } + Err(e) => { + error!("SFTP write thrown: {:?}", e); + output_producer + .send_status( + write_tracker.get_req_id(), + StatusCode::SSH_FX_FAILURE, + "error writing", + ) + .await?; + self.state = SftpHandleState::Idle; + } + }; + buf = &buf[buf.len() - source.remaining()..]; + } SftpHandleState::Initializing => { let (source, sftp_packet) = create_sftp_source_and_packet(buf); match sftp_packet { @@ -410,7 +390,8 @@ where Ok(holder) => { self.partial_write_request_tracker = Some(holder); - self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); + self.state = + SftpHandleState::ProcessingLongRequest; } Err(e) => { error!("Error handle_ran_out"); @@ -423,7 +404,7 @@ where .try_hold(&buf)?; buf = &buf[read..]; - self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); + self.state = SftpHandleState::ProcessingClippedRequest; continue; } _ => { @@ -439,19 +420,7 @@ where its length and send unsupported back", number ); - /* TODO: The packet is unknown, but we are not consuming it properly. - That has the side effect that we will be interpreting chunks of that packet - as new packets and sending more garbage back to the client - Will result on an Close and not simply a clean message saying that - - Peek out the ReqId - - Peek out the length - - flush the packet len if possible (do we have the whole length?) - - - Send an StatusCode with the relevant ReqId - - - Move on! - */ let req_id = ReqId(source.peak_packet_req_id()?); let len = source.peak_total_packet_len()? as usize; source.consume_first(len)?; @@ -485,13 +454,12 @@ where Ok(()) } - /// WIP: A loop that will process all the request from stdio until + /// Take the [`ChanInOut`] and locks, Processing all the request from stdio until /// an EOF is received pub async fn process_loop( &mut self, stdio: ChanInOut<'a>, buffer_in: &mut [u8], - // buffer_out: &'a mut [u8], ) -> SftpResult<()> { let (mut chan_in, chan_out) = stdio.split(); @@ -662,7 +630,7 @@ where }; } SftpPacket::Read(req_id, read) => { - match file_server + if let Err(error) = file_server .read( &T::try_from(&read.handle)?, read.offset, @@ -671,32 +639,19 @@ where ) .await { - Ok(()) => { - // debug!("List stats for {} is {:?}", path, attrs); - - // output_producer - // .send_packet(&SftpPacket::Attrs(req_id, attrs)) - // .await?; - } - Err(error) => { - error!("Error reading data: {:?}", error); - if let SftpError::FileServerError(status) = error { - output_producer - .send_status( - req_id, - status, - "Could not list attributes", - ) - .await?; - } else { - output_producer - .send_status( - req_id, - StatusCode::SSH_FX_FAILURE, - "Could not list attributes", - ) - .await?; - } + error!("Error reading data: {:?}", error); + if let SftpError::FileServerError(status) = error { + output_producer + .send_status(req_id, status, "Could not list attributes") + .await?; + } else { + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_FAILURE, + "Could not list attributes", + ) + .await?; } } } @@ -747,7 +702,7 @@ where /// will require to be handled differently. Gathering the data in and /// processing it as we receive it in the channel in buffer. /// - /// In the current approach a tracker is required to store the state of + /// In the current approach, a tracker is required to store the state of /// the processing of such long requests. /// /// With an implementation that where able to hold the channel_in there might @@ -824,8 +779,9 @@ where } } -/// Function to create an SFTP source and decode an SFTP packet from it -/// to avoid code duplication +/// Function to create an SFTP source and decode an SFTP packet from it. +/// +/// Defined to avoid code duplication. fn create_sftp_source_and_packet( buf: &[u8], ) -> (SftpSource<'_>, Result, WireError>) { From 1c77d6bce6a8fb80b31317149346c5ca33b77375 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 19 Nov 2025 15:08:36 +1100 Subject: [PATCH 190/393] [skip ci]some tiding up --- demo/sftp/std/src/demosftpserver.rs | 16 +++++--- sftp/src/requestholder.rs | 2 +- sftp/src/sftphandler/sftphandler.rs | 18 +++++---- sftp/src/sftpserver.rs | 61 ++++++++++++++++++++++------- sftp/src/sftpsource.rs | 2 +- 5 files changed, 69 insertions(+), 30 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index a78f8055..c44b34b2 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -17,6 +17,9 @@ use std::fs; use std::os::unix::fs::PermissionsExt; use std::{fs::File, os::unix::fs::FileExt, path::Path}; +// Used during read operations +const ARBITRARY_READ_BUFFER_LENGTH: usize = 1024; + #[derive(Debug)] pub(crate) enum PrivatePathHandle { File(PrivateFileHandle), @@ -258,7 +261,10 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { .len(); if offset >= file_len { - info!("offset is larger than file length, sending EOF"); + info!( + "offset is larger than file length, sending EOF for {:?}", + private_file_handle.path + ); reply.send_eof().await.map_err(|err| { error!("Could not sent EOF: {:?}", err); StatusCode::SSH_FX_FAILURE @@ -269,16 +275,14 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { let read_len = if file_len >= len as u64 + offset { len } else { - warn!("Read operation: length + offset > file length. Clipping ( {:?} + {:?} > {:?})", + debug!("Read operation: length + offset > file length. Clipping ( {:?} + {:?} > {:?})", len, offset, file_len); (file_len - offset).try_into().unwrap_or(u32::MAX) }; - reply.send_header(offset, read_len).await?; - - const ARBITRARY_BUFFER_LENGTH: usize = 1024; + reply.send_header(read_len).await?; - let mut read_buff = [0u8; ARBITRARY_BUFFER_LENGTH]; + let mut read_buff = [0u8; ARBITRARY_READ_BUFFER_LENGTH]; let mut running_offset = offset; let mut remaining = read_len as usize; diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs index 81dc9411..9d4a3781 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/requestholder.rs @@ -167,7 +167,7 @@ impl<'a> RequestHolder<'a> { .unwrap_or(0); if complete_to_id_index > 0 { - warn!( + debug!( "The held fragment len = {:?}, is insufficient to peak \ the length and type. Will append {:?} to reach the \ id field index: {:?}", diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 38de0b93..c271bde8 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -139,7 +139,7 @@ where { match e { RequestHolderError::RanOut => { - warn!( + debug!( "There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); @@ -148,7 +148,7 @@ where continue; } RequestHolderError::WireError(WireError::RanOut) => { - warn!( + debug!( "WIRE ERROR: There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); @@ -157,7 +157,7 @@ where continue; } RequestHolderError::NoRoom => { - warn!( + debug!( "The request holder is full but the request in is incomplete. \ We will try to decode it" ); @@ -310,7 +310,7 @@ where "", ) .await?; - info!("Finished multi part Write Request"); + debug!("Finished multi part Write Request"); self.state = SftpHandleState::Idle; } } @@ -375,8 +375,8 @@ where } Err(e) => match e { WireError::RanOut => { - warn!( - "RanOut for the SFTP Packet in the source buffer: {:?}", + debug!( + "The packet cannot fit in the receiving buffer: It will be handled in parts ({:?})", e ); @@ -394,7 +394,7 @@ where SftpHandleState::ProcessingLongRequest; } Err(e) => { - error!("Error handle_ran_out"); + debug!("Error handle_ran_out : {:?}", e); match e { SftpError::WireError( WireError::RanOut, @@ -408,6 +408,10 @@ where continue; } _ => { + error!( + "Error handle_ran_out: No Could not be catch {:?}", + e + ); return Err(SunsetError::Bug.into()); } } diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index ab58ce75..af17b38a 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,7 +1,7 @@ use crate::error::{SftpError, SftpResult}; use crate::proto::{ ENCODED_BASE_DATA_SFTP_PACKET_LENGTH, ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, - MAX_NAME_ENTRY_SIZE, NameEntry, PFlags, + MAX_NAME_ENTRY_SIZE, NameEntry, PFlags, SftpNum, SftpPacket, }; use crate::server::SftpSink; use crate::sftphandler::SftpOutputProducer; @@ -155,10 +155,21 @@ where } // TODO Define this -/// **This is a work in progress** /// A reference structure passed to the [`SftpServer::read()`] method to /// allow replying with the read data. - +/// Uses for [`ReadReply`] to: +/// +/// - In case of no more data avaliable to be sent, call `reply.send_eof()` +/// - There is data to be sent from an open file: +/// 1. Call `reply.send_header()` with the length of data to be sent +/// 2. Call `reply.send_data()` as many times as needed to complete a +/// sent of data of the announced length +/// 3. Do not call `reply.send_eof()` during this [`read`] method call +/// +/// It handles immutable sending data via the underlying sftp-channel +/// [`sunset_async::async_channel::ChanOut`] used in the context of an +/// SFTP Session. +/// pub struct ReadReply<'g, const N: usize> { /// The request Id that will be use`d in the response req_id: ReqId, @@ -178,11 +189,15 @@ impl<'g, const N: usize> ReadReply<'g, N> { } // TODO Make this enforceable - // TODO Document - pub async fn send_header(&self, offset: u64, data_len: u32) -> SftpResult<()> { + /// Sends a header fro `SSH_FXP_DATA` response. This includes the total + /// response length, the packet type, request id and data length + /// + /// The packet data content, excluding the length must be sent using + /// [`ReadReply::send_data`] + pub async fn send_header(&self, data_len: u32) -> SftpResult<()> { debug!( - "ReadReply: Sending header for request id {:?}: offset = {:?}, data length = {:?}", - self.req_id, offset, data_len + "ReadReply: Sending header for request id {:?}: data length = {:?}", + self.req_id, data_len ); let mut s = [0u8; N]; let mut sink = SftpSink::new(&mut s); @@ -190,7 +205,7 @@ impl<'g, const N: usize> ReadReply<'g, N> { // Encoding length field (data_len + ENCODED_BASE_DATA_SFTP_PACKET_LENGTH).enc(&mut sink)?; // Encoding packet type - 103u8.enc(&mut sink)?; // TODO Replace hack with + u8::from(SftpNum::SSH_FXP_DATA).enc(&mut sink)?; // Encoding req_id self.req_id.enc(&mut sink)?; // string data length @@ -207,17 +222,30 @@ impl<'g, const N: usize> ReadReply<'g, N> { Ok(()) } - /// Sends a buffer with data + /// Sends a buffer with data. Call it as many times as needed to send + /// the announced data length /// - /// Call this + /// **Important**: Call this after you have called `send_header` pub async fn send_data(&self, buff: &[u8]) -> SftpResult<()> { self.chan_out.send_data(buff).await } - /// Sends EOF meaning that there is no more files in the directory + /// Sends EOF meaning that there is no more data to be sent + /// pub async fn send_eof(&self) -> SftpResult<()> { self.chan_out.send_status(self.req_id, StatusCode::SSH_FX_EOF, "").await } + + /// Sends EOF with request id increased by one + /// + /// **Warning**: This is an experimental patch to resolve the situations where a response gets + /// stuck + /// + pub async fn send_eof_plus_one(&self) -> SftpResult<()> { + self.chan_out + .send_status(ReqId(self.req_id.0 + 1), StatusCode::SSH_FX_EOF, "") + .await + } } /// Uses for [`DirReply`] to: @@ -273,7 +301,7 @@ impl<'g, const N: usize> DirReply<'g, N> { // This way I collect data required for the header and collect // valid entries into a vector (only std) (items_encoded_len + ENCODED_BASE_NAME_SFTP_PACKET_LENGTH).enc(&mut sink)?; - 104u8.enc(&mut sink)?; // TODO Replace hack with + u8::from(SftpNum::SSH_FXP_NAME).enc(&mut sink)?; self.req_id.enc(&mut sink)?; count.enc(&mut sink)?; let payload = sink.payload_slice(); @@ -315,7 +343,11 @@ pub mod helpers { use sunset::sshwire::SSHEncode; - /// Helper function to get the length of a [`NameEntry`] + /// Helper function to get the length of a given [`NameEntry`] + /// as it would be serialized to the wire. + /// + /// Use this function to calculate the total length of a collection + /// of NameEntrys in order to send a correct response Name header pub fn get_name_entry_len(name_entry: &NameEntry<'_>) -> SftpResult { let mut buf = [0u8; MAX_NAME_ENTRY_SIZE]; let mut temp_sink = SftpSink::new(&mut buf); @@ -324,7 +356,6 @@ pub mod helpers { } } -// TODO Add this to SFTP library only available with std as a global helper #[cfg(feature = "std")] use crate::proto::Filename; #[cfg(feature = "std")] @@ -340,7 +371,7 @@ use std::{ /// /// WIP: Not stable. It has know issues and most likely it's methods will change /// -/// BUG: It does not include longname and that may be an issue +/// TODO: It does not include longname and that may be an issue #[derive(Debug)] pub struct DirEntriesCollection { /// Number of elements diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 0266dbd6..8e70c589 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -61,7 +61,7 @@ impl<'de> SftpSource<'de> { /// the result will contains garbage pub(crate) fn peak_packet_type(&self) -> WireResult { if self.buffer.len() <= SFTP_FIELD_ID_INDEX { - error!( + debug!( "Peak packet type failed: buffer len <= SFTP_FIELD_ID_INDEX ( {:?} <= {:?})", self.buffer.len(), SFTP_FIELD_ID_INDEX From 76e17127fd729ea1f0b2a3823700a0e54f31ff5c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 19 Nov 2025 15:08:59 +1100 Subject: [PATCH 191/393] adding large file 2MB to test_get.sh --- demo/sftp/std/testing/test_get.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/sftp/std/testing/test_get.sh b/demo/sftp/std/testing/test_get.sh index fa2f4c03..b46d91c4 100755 --- a/demo/sftp/std/testing/test_get.sh +++ b/demo/sftp/std/testing/test_get.sh @@ -7,7 +7,7 @@ REMOTE_HOST="192.168.69.2" REMOTE_USER="any" # Define test files -FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random") +FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random" "2048kB_random") # Generate random data files echo "Generating random data files..." From 58067115b52dcd913ad302ea0dee6600a9139453 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 19 Nov 2025 15:16:13 +1100 Subject: [PATCH 192/393] [skip ci] Bug? Large write buffers cause for Chan_out cause ssh.server.progress fail: NoRoom Running `sunset_demo_sftp_std` and calling test_get.sh triggers this error from time to time --- demo/sftp/std/src/main.rs | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 1ba35e54..46668265 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -92,7 +92,14 @@ impl DemoServer for StdDemo { let ssh_loop_inner = async { loop { let mut ph = ProgressHolder::new(); - let ev = serv.progress(&mut ph).await?; + let ev = match serv.progress(&mut ph).await { + Ok(event) => event, + Err(e) => { + error!("server progress failed: {:?}", e); // NoRoom: 2048 Bytes Output buffer + return Err(e); + } + }; + trace!("ev {ev:?}"); match ev { ServEvent::SessionShell(a) => { @@ -148,7 +155,7 @@ impl DemoServer for StdDemo { // TODO Do some research to find reasonable default buffer lengths let mut buffer_in = [0u8; 512]; - let mut incomplete_request_buffer = [0u8; 256]; + let mut incomplete_request_buffer = [0u8; 512]; match { let stdio = serv.stdio(ch).await?; @@ -156,7 +163,7 @@ impl DemoServer for StdDemo { "./demo/sftp/std/testing/out/".to_string(), ); - SftpHandler::::new( + SftpHandler::::new( &mut file_server, &mut incomplete_request_buffer, ) @@ -206,15 +213,17 @@ async fn listen( async fn main(spawner: Spawner) { env_logger::builder() .filter_level(log::LevelFilter::Info) - .filter_module( - "sunset_demo_sftp_std::demosftpserver", - log::LevelFilter::Debug, - ) - .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) - .filter_module( - "sunset_sftp::sftphandler::sftpoutputchannelhandler", - log::LevelFilter::Debug, - ) + // .filter_module( + // "sunset_demo_sftp_std::demosftpserver", + // log::LevelFilter::Debug, + // ) + // .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) + // .filter_module("sunset_sftp", log::LevelFilter::Trace) + // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Debug) + // .filter_module( + // "sunset_sftp::sftphandler::sftpoutputchannelhandler", + // log::LevelFilter::Debug, + // ) // .filter_module("sunset::channel", log::LevelFilter::Trace) // .filter_module("sunset_async::async_sunset", log::LevelFilter::Trace) // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) From e41dcebfaebce769a2551114eb4719876dc3b679 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 19 Nov 2025 16:51:47 +1100 Subject: [PATCH 193/393] [skip ci] Debugging runner.read_channel and .input Still haven't found where the missing requests are. If I am sending bad data, I am still expecting to receive the data already sent according to the SFTP client New logs can be found running sftp_std demo and ./test_get.sh --- demo/sftp/std/src/main.rs | 22 ++++++++++++---------- src/runner.rs | 12 +++++++++++- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 46668265..5e8f9eb5 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -163,7 +163,7 @@ impl DemoServer for StdDemo { "./demo/sftp/std/testing/out/".to_string(), ); - SftpHandler::::new( + SftpHandler::::new( &mut file_server, &mut incomplete_request_buffer, ) @@ -213,19 +213,21 @@ async fn listen( async fn main(spawner: Spawner) { env_logger::builder() .filter_level(log::LevelFilter::Info) - // .filter_module( - // "sunset_demo_sftp_std::demosftpserver", - // log::LevelFilter::Debug, - // ) - // .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) + .filter_module( + "sunset_demo_sftp_std::demosftpserver", + log::LevelFilter::Debug, + ) + .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) // .filter_module("sunset_sftp", log::LevelFilter::Trace) // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Debug) - // .filter_module( - // "sunset_sftp::sftphandler::sftpoutputchannelhandler", - // log::LevelFilter::Debug, - // ) + .filter_module( + "sunset_sftp::sftphandler::sftpoutputchannelhandler", + log::LevelFilter::Info, + ) // .filter_module("sunset::channel", log::LevelFilter::Trace) // .filter_module("sunset_async::async_sunset", log::LevelFilter::Trace) + .filter_module("sunset::runner", log::LevelFilter::Debug) + // .filter_module("sunset::traffic", log::LevelFilter::Trace) // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) // .filter_module("sunset_sftp::sftpserver", log::LevelFilter::Info) diff --git a/src/runner.rs b/src/runner.rs index 6cfcb9b1..b7b23e7b 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -387,7 +387,13 @@ impl<'a, CS: CliServ> Runner<'a, CS> { if !self.is_input_ready() { return Ok(0); } - self.traf_in.input(&mut self.keys, &mut self.conn.remote_version, buf) + + self.traf_in + .input(&mut self.keys, &mut self.conn.remote_version, buf) + .inspect(|value| { + // TODO Jubeor. Remove this inspection + debug!("wire input bytes ({:?}): {:?}", *value, &buf[..*value]) + }) } // Whether [`input()`](input) is ready @@ -525,6 +531,10 @@ impl<'a, CS: CliServ> Runner<'a, CS> { } let (len, complete) = self.traf_in.read_channel(chan.0, dt, buf); + // TODO Jubeor. Remove this if block + if len != 0 { + debug!("read_channel buff ({len:?}): {:?}", &buf[..len]); + } if let Some(x) = complete { self.finished_read_channel(chan, x)?; } From e09c03cfea9c2e191b82e4a74d5410e8c83739cd Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 19 Nov 2025 21:59:11 +0800 Subject: [PATCH 194/393] Fix building with backtrace feature Test it in CI as well. Fixes: 7bea15b0251d ("Give Kex a CliServ generic parameter") Fixes: b5e56871b4b5 ("Fix building with backtrace feature") --- src/error.rs | 3 +++ src/runner.rs | 2 +- testing/ci.sh | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/error.rs b/src/error.rs index a27dcb7d..42333e8b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,6 +8,9 @@ use snafu::prelude::*; use crate::channel::ChanNum; +#[allow(unused_imports)] +use snafu::{Backtrace, Location}; + // TODO: can we make Snafu not require Debug? /// The Sunset error type. diff --git a/src/runner.rs b/src/runner.rs index 21faf861..fb65c2d0 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -693,7 +693,7 @@ impl<'a, CS: CliServ> Runner<'a, CS> { self.conn.channels.term_window_change(chan.0, winch, &mut s) } else { trace!("winch as server"); - Err(Error::BadUsage {}) + Err(error::BadUsage.build()) } } diff --git a/testing/ci.sh b/testing/ci.sh index 733413f7..22b540a1 100755 --- a/testing/ci.sh +++ b/testing/ci.sh @@ -42,7 +42,8 @@ cargo test --doc cd stdasync # only test lib since some examples are broken cargo test --lib -cargo build --example sunsetc +# test backtrace feature too +cargo build --example sunsetc --features sunset/backtrace # with/without release to test debug_assertions cargo build --release --example sunsetc ) From e2774a95c6417ca173d5248520676e7ca5830af3 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 19 Nov 2025 23:07:13 +0800 Subject: [PATCH 195/393] traffic: Fix accidentally public payload() Was probably left from debugging. --- src/traffic.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/traffic.rs b/src/traffic.rs index 7f4b4d14..c9551fe0 100644 --- a/src/traffic.rs +++ b/src/traffic.rs @@ -154,9 +154,7 @@ impl<'a> TrafIn<'a> { /// Returns a reference to the decrypted payload buffer if ready, /// and the `seq` of that packet. - // TODO: only pub for testing - // pub(crate) fn payload(&mut self) -> Option<(&[u8], u32)> { - pub fn payload(&self) -> Option<(&[u8], u32)> { + pub(crate) fn payload(&self) -> Option<(&[u8], u32)> { match self.state { RxState::InPayload { len, seq } => { let payload = &self.buf[SSH_PAYLOAD_START..SSH_PAYLOAD_START + len]; From e05ffb6cb96e173b306f41bf31b87f6a20fff5b2 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 19 Nov 2025 22:37:34 +0800 Subject: [PATCH 196/393] async: Fix discarded channel input data Channel input was inadvertently being discarded because of an incorrect refcount comparison (wrong order of operations). This would cause data to be discarded if async progress() runs before the ChanIn has consumed data. In sunsetc this never seems to occur, maybe due to specifics how the Futures are executed by embassy_futures::select? In work in progress SFTP's demo, incoming data seems to be intermittently discarded. Fixes: f06f7cea77b8 ("Move channel wakers into core sunset crate") --- async/src/async_sunset.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/async/src/async_sunset.rs b/async/src/async_sunset.rs index cabba933..9e7bd6a4 100644 --- a/async/src/async_sunset.rs +++ b/async/src/async_sunset.rs @@ -214,7 +214,7 @@ impl<'a, CS: CliServ> AsyncSunset<'a, CS> { fn discard_channels(&self, inner: &mut Inner) -> Result<()> { if let Some((num, dt, _len)) = inner.runner.read_channel_ready() { - if !self.chan_readcount(num, dt).load(Acquire) > 0 { + if self.chan_readcount(num, dt).load(Acquire) == 0 { // There are no live ChanIn or ChanInOut for the num/dt, // so nothing will read the channel. // Discard the data so it doesn't block forever. From ecd64f9838359bcca44abb5a7326d0c09e72d666 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 19 Nov 2025 21:59:11 +0800 Subject: [PATCH 197/393] Fix building with backtrace feature Test it in CI as well. Fixes: 7bea15b0251d ("Give Kex a CliServ generic parameter") Fixes: b5e56871b4b5 ("Fix building with backtrace feature") --- src/error.rs | 5 ++++- src/runner.rs | 2 +- testing/ci.sh | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/error.rs b/src/error.rs index c9a0f433..e0409678 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,12 +5,15 @@ use log::{debug, error, info, log, trace, warn}; use core::fmt; use core::fmt::Arguments; -use snafu::{prelude::*, Backtrace, Location}; +use snafu::prelude::*; use heapless::String; use crate::channel::ChanNum; +#[allow(unused_imports)] +use snafu::{Backtrace, Location}; + // TODO: can we make Snafu not require Debug? /// The Sunset error type. diff --git a/src/runner.rs b/src/runner.rs index b7b23e7b..0404dd0c 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -698,7 +698,7 @@ impl<'a, CS: CliServ> Runner<'a, CS> { self.conn.channels.term_window_change(chan.0, winch, &mut s) } else { trace!("winch as server"); - Err(Error::BadUsage {}) + Err(error::BadUsage.build()) } } diff --git a/testing/ci.sh b/testing/ci.sh index 733413f7..22b540a1 100755 --- a/testing/ci.sh +++ b/testing/ci.sh @@ -42,7 +42,8 @@ cargo test --doc cd stdasync # only test lib since some examples are broken cargo test --lib -cargo build --example sunsetc +# test backtrace feature too +cargo build --example sunsetc --features sunset/backtrace # with/without release to test debug_assertions cargo build --release --example sunsetc ) From d763b4ee207554666a95c3732fde941e5ee9038a Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 19 Nov 2025 22:04:33 +0800 Subject: [PATCH 198/393] sftp: Fix building with sunset backtrace --- sftp/src/sftperror.rs | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index 8fd251d9..43edd609 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -81,29 +81,14 @@ impl From for SunsetError { match value { SftpError::SunsetError(error) => error, SftpError::WireError(wire_error) => wire_error.into(), - SftpError::NotInitialized => { + SftpError::NotInitialized + | SftpError::NotSupported + | SftpError::AlreadyInitialized + | SftpError::MalformedPacket + | SftpError::RequestHolderError(_) + | SftpError::FileServerError(_) => { warn!("Casting error loosing information: {:?}", value); - SunsetError::PacketWrong {} - } - SftpError::NotSupported => { - warn!("Casting error loosing information: {:?}", value); - SunsetError::PacketWrong {} - } - SftpError::AlreadyInitialized => { - warn!("Casting error loosing information: {:?}", value); - SunsetError::PacketWrong {} - } - SftpError::MalformedPacket => { - warn!("Casting error loosing information: {:?}", value); - SunsetError::PacketWrong {} - } - SftpError::RequestHolderError(_) => { - warn!("Casting error loosing information: {:?}", value); - SunsetError::Bug - } - SftpError::FileServerError(_) => { - warn!("Casting error loosing information: {:?}", value); - SunsetError::Bug + sunset::error::PacketWrong.build() } SftpError::ClientDisconnected => SunsetError::ChannelEOF, } From 748d9e6d0b1f345785c2e85ebbe198002f6a58ab Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 19 Nov 2025 22:37:34 +0800 Subject: [PATCH 199/393] async: Fix discarded channel input data Channel input was inadvertently being discarded because of an incorrect refcount comparison (wrong order of operations). This would cause data to be discarded if async progress() runs before the ChanIn has consumed data. In sunsetc this never seems to occur, maybe due to specifics how the Futures are executed by embassy_futures::select? In work in progress SFTP's demo, incoming data seems to be intermittently discarded. Fixes: f06f7cea77b8 ("Move channel wakers into core sunset crate") --- async/src/async_sunset.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/async/src/async_sunset.rs b/async/src/async_sunset.rs index cabba933..9e7bd6a4 100644 --- a/async/src/async_sunset.rs +++ b/async/src/async_sunset.rs @@ -214,7 +214,7 @@ impl<'a, CS: CliServ> AsyncSunset<'a, CS> { fn discard_channels(&self, inner: &mut Inner) -> Result<()> { if let Some((num, dt, _len)) = inner.runner.read_channel_ready() { - if !self.chan_readcount(num, dt).load(Acquire) > 0 { + if self.chan_readcount(num, dt).load(Acquire) == 0 { // There are no live ChanIn or ChanInOut for the num/dt, // so nothing will read the channel. // Discard the data so it doesn't block forever. From 286950160ccafbf39d5f9c3068994b5d0f070ab0 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 19 Nov 2025 23:27:04 +0800 Subject: [PATCH 200/393] Workaround for NoRoom sending window adjustment This is a hack workaround to ignore failures sending a window adjustment - hopefully more data is yet to come so another window adjustment can be sent later. A proper fix needs to be implemented. --- src/channel.rs | 18 ++++++++++++++---- src/encrypt.rs | 4 +++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/channel.rs b/src/channel.rs index 7fcf6de5..30b943bd 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -219,7 +219,16 @@ impl Channels { let ch = self.get_mut(num)?; ch.finished_input(len); if let Some(w) = ch.check_window_adjust()? { - s.send(w)?; + // The send buffer may be full. Ignore the failure and hope another adjustment is + // sent later. TODO improve this. + match s.send(w) { + Ok(_) => ch.pending_adjust = 0, + Err(Error::NoRoom { .. }) => { + // TODO better retry rather than hoping a retry occurs + debug!("noroom for adjustment") + } + error => return error, + } } Ok(()) } @@ -1009,11 +1018,12 @@ impl Channel { } /// Returns a window adjustment packet if required - fn check_window_adjust(&mut self) -> Result>> { - let num = self.send.as_mut().trap()?.num; + /// + /// Does not reset the adjustment to 0, should be done by caller on successful send. + fn check_window_adjust(&self) -> Result>> { + let num = self.send.as_ref().trap()?.num; if self.pending_adjust > self.full_window / 2 { let adjust = self.pending_adjust as u32; - self.pending_adjust = 0; let p = packets::ChannelWindowAdjust { num, adjust }.into(); Ok(Some(p)) } else { diff --git a/src/encrypt.rs b/src/encrypt.rs index 40cc3a99..d40a4932 100644 --- a/src/encrypt.rs +++ b/src/encrypt.rs @@ -135,7 +135,9 @@ impl KeyState { buf: &mut [u8], ) -> Result { let e = self.enc.encrypt(payload_len, buf, self.seq_encrypt.0); - self.seq_encrypt += 1; + if !matches!(e, Err(Error::NoRoom { .. })) { + self.seq_encrypt += 1; + } e } From 5c53bfab2a256c492a516e74a6323ad34b7d8f26 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sun, 14 Jul 2024 13:39:24 +0800 Subject: [PATCH 201/393] Start on sftp proto definitions --- Cargo.toml | 1 + sftp/Cargo.toml | 6 ++ sftp/src/lib.rs | 1 + sftp/src/sftp-proto.rs | 125 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 sftp/Cargo.toml create mode 100644 sftp/src/lib.rs create mode 100644 sftp/src/sftp-proto.rs diff --git a/Cargo.toml b/Cargo.toml index df2cb287..4e0c6627 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ rust-version = "1.87" members = [ "demo/picow", "demo/std", "fuzz", + "sftp", "stdasync", # workspace.dependencies paths are automatic ] diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml new file mode 100644 index 00000000..a5e8b996 --- /dev/null +++ b/sftp/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "sunset-sftp" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs new file mode 100644 index 00000000..2447730e --- /dev/null +++ b/sftp/src/lib.rs @@ -0,0 +1 @@ +mod sftp_proto; diff --git a/sftp/src/sftp-proto.rs b/sftp/src/sftp-proto.rs new file mode 100644 index 00000000..bb80a135 --- /dev/null +++ b/sftp/src/sftp-proto.rs @@ -0,0 +1,125 @@ +#[derive(SSHEncode, SSHDecode)] +struct Init { + version: u32, + // TODO extensions +} + +#[derive(SSHEncode, SSHDecode)] +struct Version { + version: u32, + // TODO extensions +} + +struct FileAttrs { + flags: u32, + size: u64, + uid: Option, + gid: Option, + permissions: Option, + atime: Option, + mtime: Option, + // TODO extended +} + +#[derive(SSHEncode, SSHDecode)] +struct Open { + id: u32, + // TODO or TextString + filename: BinString, + pflags: 32, + attrs: FileAttrs, +} + +#[repr(u32)] +enum Pflags { + Read = 1, + Write = 2, + Append = 4, + Creat = 8, + Trunc = 16, + Excl = 32, +} + +#[derive(SSHEncode, SSHDecode)] +struct Close { + id: u32, + handle: BinString, +} + +#[derive(SSHEncode, SSHDecode)] +struct Read { + id: u32, + handle: BinString, + offset: u64, + len: u32, +} + +#[derive(SSHEncode, SSHDecode)] +struct Write { + id: u32, + handle: BinString, + offset: u64, + data: BinString, +} + +#[derive(SSHEncode, SSHDecode)] +struct Remove { + id: u32, + filename: BinString, +} + +#[derive(SSHEncode, SSHDecode)] +struct Rename { + id: u32, + old: BinString, + new: BinString, +} + +#[derive(SSHEncode, SSHDecode)] +struct Mkdir { + id: u32, + path: BinString, + attrs: FileAttrs, +} + + +#[derive(SSHEncode, SSHDecode)] +struct Status { + id: u32, + status_code: u32, + msg: TextString, + lang: &str, +} + +#[repr(u32)] +enum StatusCode { + Ok = 1, + Eof = 2, + NoSuchFile = 3, + PermissionDenied = 4, + Failure = 5, + BadMessage = 6, + NoConnection = 7, + ConnectionLost = 8, + OpUnsupported = 9, +} + +#[derive(SSHEncode, SSHDecode)] +struct Handle { + id: u32, + handle: BinString, +} + +#[derive(SSHEncode, SSHDecode)] +struct Data { + id: u32, + handle: BinString, + offset: u64, + data: BinString, +} + +#[derive(SSHEncode, SSHDecode)] +struct Name<'a> { + id: u32, + names: &'a [i] +} From f9d690457b1c197857342d199e9c0fe137c07270 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 31 May 2025 13:53:09 +0800 Subject: [PATCH 202/393] Add more changes from March 2025 --- sftp/Cargo.toml | 4 +- sftp/src/lib.rs | 3 +- sftp/src/proto.rs | 328 +++++++++++++++++++++++++++++++++++++++++ sftp/src/sftp-proto.rs | 125 ---------------- sftp/src/sftpserver.rs | 35 +++++ 5 files changed, 368 insertions(+), 127 deletions(-) create mode 100644 sftp/src/proto.rs delete mode 100644 sftp/src/sftp-proto.rs create mode 100644 sftp/src/sftpserver.rs diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index a5e8b996..d585b97c 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -1,6 +1,8 @@ [package] name = "sunset-sftp" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] +sunset = { version = "0.2.0", path = "../" } +sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 2447730e..4c2f3ffe 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1 +1,2 @@ -mod sftp_proto; +mod proto; +mod sftpserver; diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs new file mode 100644 index 00000000..9c5928df --- /dev/null +++ b/sftp/src/proto.rs @@ -0,0 +1,328 @@ +use core::marker::PhantomData; + +use sshwire::{BinString, TextString, SSHEncode, SSHDecode, SSHSource, SSHSink, WireResult, WireError}; +use sunset::{error, Result}; + +// TODO is utf8 enough, or does this need to be an opaque binstring? +#[derive(Debug)] +pub struct Filename<'a>(TextString<'a>); + +#[derive(Debug, SSHEncode, SSHDecode)] +struct FileHandle<'a>(pub BinString<'a>); + +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct InitVersion<'a> { + // No ReqId for SSH_FXP_INIT + pub version: u32, + // TODO variable number of ExtPair + pub _ext: &'a PhantomData<()>, +} + +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct Open<'a> { + pub filename: Filename<'a>, + pub pflags: u32, + pub attrs: Attrs<'a>, +} + +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct Close<'a> { + pub handle: FileHandle<'a>, +} + +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct Read<'a> { + pub handle: FileHandle<'a>, + pub offset: u64, + pub len: u32, +} + +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct Write<'a> { + pub handle: FileHandle<'a>, + pub offset: u64, + pub data: BinString<'a>, +} + +// Responses + +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct Status<'a> { + pub code: StatusCode, + pub message: TextString<'a>, + pub lang: TextString<'a>, +} + +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct Handle<'a> { + pub handle: FileHandle<'a>, +} + +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct Data<'a> { + pub handle: FileHandle<'a>, + pub offset: u64, + pub data: BinString<'a>, +} + +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct Name<'a> { + pub count: u32, + // TODO repeat NameEntry +} + +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct NameEntry<'a> { + pub filename: Filename<'a>, + /// longname is an undefined text line like "ls -l", + /// SHOULD NOT be used. + pub _longname: Filename<'a>, + pub attrs: Attrs<'a>, +} + +#[derive(Debug, SSHEncode, SSHDecode, Clone, Copy)] +pub struct ReqId(pub u32); + +#[derive(Debug, SSHEncode, SSHDecode)] +#[repr(u8)] +#[allow(non_camel_case_types)] +enum StatusCode { + SSH_FX_OK = 0, + SSH_FX_EOF = 1, + SSH_FX_NO_SUCH_FILE = 2, + SSH_FX_PERMISSION_DENIED = 3, + SSH_FX_FAILURE = 4, + SSH_FX_BAD_MESSAGE = 5, + SSH_FX_NO_CONNECTION = 6, + SSH_FX_CONNECTION_LOST = 7, + SSH_FX_OP_UNSUPPORTED = 8, + Other(u8), +} + +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct ExtPair<'a> { + pub name: &'a str, + pub data: BinString<'a>, +} + +#[derive(Debug)] +pub struct Attrs<'a> { + // flags: u32, defines used attributes + pub size: Option, + pub uid: Option, + pub gid: Option, + pub permissions: Option, + pub atime: Option, + pub mtime: Option, + pub ext_count: Option, + // TODO extensions +} + +enum Error { + UnknownPacket { number: u8 }, +} + +macro_rules! sftpmessages { + ( + $( ( $message_num:literal, + $SpecificPacketVariant:ident, + $SpecificPacketType:ty, + $SSH_FXP_NAME:ident + ), + )* + ) => { + + +#[derive(Debug)] +#[repr(u8)] +#[allow(non_camel_case_types)] +pub enum SftpNum { + // variants are eg + // SSH_FXP_OPEN = 3, + $( + $SSH_FXP_NAME = $message_num, + )* +} + +impl SftpNum { + fn is_request(&self) -> bool { + // TODO SSH_FXP_EXTENDED + (2..=99).contains(self as u8) + } + + fn is_response(&self) -> bool { + // TODO SSH_FXP_EXTENDED_REPLY + (100..=199).contains(self as u8) + } +} + +impl TryFrom for SftpNum { + type Error = Error; + fn try_from(v: u8) -> Result { + match v { + // eg + // 3 => Ok(SftpNum::SSH_FXP_OPEN) + $( + $message_num => Ok(SftpNum::$SSH_FXP_NAME), + )* + _ => { + Err(Error::UnknownPacket { number: v }) + } + } + } +} + +impl SSHEncode for SftpPacket<'_> { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + let t = self.message_num() as u8; + t.enc(s)?; + match self { + // eg + // Packet::KexInit(p) => { + // ... + $( + Packet::$SpecificPacketVariant(p) => { + p.enc(s)? + } + )* + }; + Ok(()) + } +} + +impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { + fn dec(s: &mut S) -> WireResult + where S: SSHSource<'de> { + let msg_num = u8::dec(s)?; + let ty = MessageNumber::try_from(msg_num); + let ty = match ty { + Ok(t) => t, + Err(_) => return Err(WireError::UnknownPacket { number: msg_num }) + }; + + // Decode based on the message number + let p = match ty { + // eg + // MessageNumber::SSH_MSG_KEXINIT => Packet::KexInit( + // ... + $( + MessageNumber::$SSH_MESSAGE_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), + )* + }; + Ok(p) + } +} + +/// Top level SSH packet enum +#[derive(Debug)] +pub enum SftpPacket<'a> { + // eg Open(Open<'a>), + $( + $SpecificPacketVariant($SpecificPacketType), + )* +} + +impl<'a> SftpPacket<'a> { + pub fn sftp_num(&self) -> SftpNum { + match self { + // eg + // SftpPacket::Open(_) => { + // .. + $( + SftpPacket::$SpecificPacketVariant(_) => { + MessageNumber::$SSH_FXP_NAME + } + )* + } + } + + /// Encode a request. + /// + /// Used by a SFTP client. Does not include the length field. + pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> { + if !self.sftp_num().is_request() { + return Err(Error::bug()) + } + + // packet type + self.sftp_num().enc(s)?; + // request ID + id.0.enc(s)?; + // contents + self.enc(s) + } + + /// Decode a response. + /// + /// Used by a SFTP client. Does not include the length field. + pub fn decode_response(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> { + let num = SftpNum::try_from(u8::dec(s)?)?; + + if !num.is_response() { + return error::SSHProto.fail(); + } + + let id = ReqId(u32::dec(s)?); + Ok((id, Self::dec(s))) + } + + /// Decode a request. + /// + /// Used by a SFTP server. Does not include the length field. + pub fn decode_request(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> { + let num = SftpNum::try_from(u8::dec(s)?)?; + + if !num.is_request() { + return error::SSHProto.fail(); + } + + let id = ReqId(u32::dec(s)?); + Ok((id, Self::dec(s))) + } + + /// Encode a response. + /// + /// Used by a SFTP server. Does not include the length field. + pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> { + if !self.sftp_num().is_response() { + return Err(Error::bug()) + } + + // packet type + self.sftp_num().enc(s)?; + // request ID + id.0.enc(s)?; + // contents + self.enc(s) + } +} + +$( +impl<'a> From<$SpecificPacketType> for SftpPacket<'a> { + fn from(s: $SpecificPacketType) -> SftpPacket<'a> { + SftpPacket::$SpecificPacketVariant(s) + } +} +)* + +} } // macro + +sftpmessages![ + +// Message number ranges are also used by Sftpnum::is_request and is_response. + +(1, Init, InitVersion<'a>, SSH_FXP_INIT), +(2, Version, InitVersion<'a>, SSH_FXP_VERSION), + +// Requests +(3, Open, Open<'a>, SSH_FXP_OPEN), +(4, Close, Close<'a>, SSH_FXP_CLOSE), +(5, Read, Read<'a>, SSH_FXP_READ), + +// Responses +(101, Status, Status<'a>, SSH_FXP_STATUS), +(102, Handle, Handle<'a>, SSH_FXP_HANDLE), +(103, Data, Data<'a>, SSH_FXP_DATA), +(104, Name, Name<'a>, SSH_FXP_NAME), + +]; diff --git a/sftp/src/sftp-proto.rs b/sftp/src/sftp-proto.rs deleted file mode 100644 index bb80a135..00000000 --- a/sftp/src/sftp-proto.rs +++ /dev/null @@ -1,125 +0,0 @@ -#[derive(SSHEncode, SSHDecode)] -struct Init { - version: u32, - // TODO extensions -} - -#[derive(SSHEncode, SSHDecode)] -struct Version { - version: u32, - // TODO extensions -} - -struct FileAttrs { - flags: u32, - size: u64, - uid: Option, - gid: Option, - permissions: Option, - atime: Option, - mtime: Option, - // TODO extended -} - -#[derive(SSHEncode, SSHDecode)] -struct Open { - id: u32, - // TODO or TextString - filename: BinString, - pflags: 32, - attrs: FileAttrs, -} - -#[repr(u32)] -enum Pflags { - Read = 1, - Write = 2, - Append = 4, - Creat = 8, - Trunc = 16, - Excl = 32, -} - -#[derive(SSHEncode, SSHDecode)] -struct Close { - id: u32, - handle: BinString, -} - -#[derive(SSHEncode, SSHDecode)] -struct Read { - id: u32, - handle: BinString, - offset: u64, - len: u32, -} - -#[derive(SSHEncode, SSHDecode)] -struct Write { - id: u32, - handle: BinString, - offset: u64, - data: BinString, -} - -#[derive(SSHEncode, SSHDecode)] -struct Remove { - id: u32, - filename: BinString, -} - -#[derive(SSHEncode, SSHDecode)] -struct Rename { - id: u32, - old: BinString, - new: BinString, -} - -#[derive(SSHEncode, SSHDecode)] -struct Mkdir { - id: u32, - path: BinString, - attrs: FileAttrs, -} - - -#[derive(SSHEncode, SSHDecode)] -struct Status { - id: u32, - status_code: u32, - msg: TextString, - lang: &str, -} - -#[repr(u32)] -enum StatusCode { - Ok = 1, - Eof = 2, - NoSuchFile = 3, - PermissionDenied = 4, - Failure = 5, - BadMessage = 6, - NoConnection = 7, - ConnectionLost = 8, - OpUnsupported = 9, -} - -#[derive(SSHEncode, SSHDecode)] -struct Handle { - id: u32, - handle: BinString, -} - -#[derive(SSHEncode, SSHDecode)] -struct Data { - id: u32, - handle: BinString, - offset: u64, - data: BinString, -} - -#[derive(SSHEncode, SSHDecode)] -struct Name<'a> { - id: u32, - names: &'a [i] -} diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs new file mode 100644 index 00000000..6623b989 --- /dev/null +++ b/sftp/src/sftpserver.rs @@ -0,0 +1,35 @@ +use proto::{StatusCode, Attrs}; + +pub type Result = core::result::Result; + +/// All trait functions are optional in the SFTP protocol. +/// Some less core operations have a Provided implementation returning +/// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, +/// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. +trait SftpServer { + type Handle; + + // TODO flags struct + async fn open(filename: &str, flags: u32, attrs: &Attrs) -> Result; + + /// Close either a file or directory handle + async fn close(handle: &Self::Handle) -> Result<()>; + + async fn read(handle: &Self::Handle, offset: u64, reply: &mut ReadReply) -> Result<()>; + + async fn write(handle: &Self::Handle, offset: u64, buf: &[u8]) -> Result<()>; + + async fn opendir(dir: &str) -> Result; + + async fn readdir(handle: &Self::Handle, reply: &mut DirReply) -> Result<()>; +} + +pub struct ReadReply<'g, 'a> { + chan: ChanOut<'g, 'a>, +} + +impl<'g, 'a> ReadReply<'g, 'a> { + pub async fn reply(self, data: &[u8]) { + + } +} From cf031fdd22f6861cc1006bd32f6453ebd5ab709e Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 15 Aug 2025 14:45:36 +1000 Subject: [PATCH 203/393] Bumping the sunset version to match the main project --- sftp/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index d585b97c..43385160 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -4,5 +4,5 @@ version = "0.1.0" edition = "2024" [dependencies] -sunset = { version = "0.2.0", path = "../" } +sunset = { version = "0.3.0", path = "../" } sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } From 56d9d9c2da95b1eea86b8286bd7bb19bdc1aa744 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 11:58:29 +1000 Subject: [PATCH 204/393] cargo fmt --- sftp/src/proto.rs | 7 +++++-- sftp/src/sftpserver.rs | 15 +++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 9c5928df..c8a58487 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,7 +1,10 @@ use core::marker::PhantomData; -use sshwire::{BinString, TextString, SSHEncode, SSHDecode, SSHSource, SSHSink, WireResult, WireError}; -use sunset::{error, Result}; +use sshwire::{ + BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, + WireResult, +}; +use sunset::{Result, error}; // TODO is utf8 enough, or does this need to be an opaque binstring? #[derive(Debug)] diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 6623b989..0ce22383 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,4 +1,4 @@ -use proto::{StatusCode, Attrs}; +use proto::{Attrs, StatusCode}; pub type Result = core::result::Result; @@ -10,12 +10,17 @@ trait SftpServer { type Handle; // TODO flags struct - async fn open(filename: &str, flags: u32, attrs: &Attrs) -> Result; + async fn open(filename: &str, flags: u32, attrs: &Attrs) + -> Result; /// Close either a file or directory handle async fn close(handle: &Self::Handle) -> Result<()>; - async fn read(handle: &Self::Handle, offset: u64, reply: &mut ReadReply) -> Result<()>; + async fn read( + handle: &Self::Handle, + offset: u64, + reply: &mut ReadReply, + ) -> Result<()>; async fn write(handle: &Self::Handle, offset: u64, buf: &[u8]) -> Result<()>; @@ -29,7 +34,5 @@ pub struct ReadReply<'g, 'a> { } impl<'g, 'a> ReadReply<'g, 'a> { - pub async fn reply(self, data: &[u8]) { - - } + pub async fn reply(self, data: &[u8]) {} } From a0a7eeba02b6290a2beb7f8d3c573847b0e74d75 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:01:02 +1000 Subject: [PATCH 205/393] sunset-sftp added to Cargo.lock This was an automatic resolution --- Cargo.lock | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 86712c2a..80509a49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2876,6 +2876,14 @@ dependencies = [ "sunset-sshwire-derive", ] +[[package]] +name = "sunset-sftp" +version = "0.1.0" +dependencies = [ + "sunset", + "sunset-sshwire-derive", +] + [[package]] name = "sunset-sshwire-derive" version = "0.2.0" From cf480e9f55e824d6e0b5d52ad50d16c5fd795949 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:01:49 +1000 Subject: [PATCH 206/393] error fix on casting SftpNum to u8 Fixing error message `casting `&SftpNum` as `u8` is invalid: needs casting through a raw pointer firstrust-analyzerE0606` --- sftp/src/proto.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index c8a58487..aab623ab 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -150,12 +150,12 @@ pub enum SftpNum { impl SftpNum { fn is_request(&self) -> bool { // TODO SSH_FXP_EXTENDED - (2..=99).contains(self as u8) + (2..=99).contains(*self as u8) } fn is_response(&self) -> bool { // TODO SSH_FXP_EXTENDED_REPLY - (100..=199).contains(self as u8) + (100..=199).contains(*self as u8) } } From 196908358a3d083db5b3c4e97881dff7127ca453 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:18:42 +1000 Subject: [PATCH 207/393] fixing error in sftpmessages macro_rules SSHDecode implementation I believe that $SSH_MESSAGE_NAME should be $SSH_FXP_NAME for this macro to work and that it was intended for this match ty expansion. --- sftp/src/proto.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index aab623ab..f463ad49 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -208,8 +208,11 @@ impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { // eg // MessageNumber::SSH_MSG_KEXINIT => Packet::KexInit( // ... + // Error: MessageNumber::$SSH_MESSAGE_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), + // ^^^^^^^^^^^^^^^^^ expected identifier + // Changed SSH_MESSAGE_NAME to SSH_FXP_NAME. I believe that this is the intended thing to do $( - MessageNumber::$SSH_MESSAGE_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), + MessageNumber::$SSH_FXP_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), )* }; Ok(p) From c97f6e36333ab902b07c1b029df0dfa831e7feec Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:20:49 +1000 Subject: [PATCH 208/393] fixing use of sftp proto --- sftp/src/sftpserver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 0ce22383..7b119849 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,4 +1,4 @@ -use proto::{Attrs, StatusCode}; +use crate::proto::{Attrs, StatusCode}; pub type Result = core::result::Result; From 9fb70f72c23473f2832cb88e1be5526f37277b71 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:34:46 +1000 Subject: [PATCH 209/393] Adding some extra uses in proto.rs --- sftp/src/proto.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index f463ad49..c3aa680c 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,11 +1,15 @@ use core::marker::PhantomData; -use sshwire::{ +use sunset::packets::{MessageNumber, Packet}; +use sunset::sshwire::{ BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, WireResult, }; use sunset::{Result, error}; +use sunset_sshwire_derive::{SSHDecode, SSHEncode}; +use sunset::{Result, error}; + // TODO is utf8 enough, or does this need to be an opaque binstring? #[derive(Debug)] pub struct Filename<'a>(TextString<'a>); From e1f449c64342fa1fcdfacfa4fd0724e1d794b368 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:37:01 +1000 Subject: [PATCH 210/393] making proto StatusCode pub --- sftp/src/proto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index c3aa680c..a016d9c2 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -93,7 +93,7 @@ pub struct ReqId(pub u32); #[derive(Debug, SSHEncode, SSHDecode)] #[repr(u8)] #[allow(non_camel_case_types)] -enum StatusCode { +pub enum StatusCode { SSH_FX_OK = 0, SSH_FX_EOF = 1, SSH_FX_NO_SUCH_FILE = 2, From 5175016f82781111655be9238b9f13554793de4f Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:38:27 +1000 Subject: [PATCH 211/393] Fixing sftpmessages SftpNum impl issue providing the correct data type to contains (a reference to a u8) --- sftp/src/proto.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index a016d9c2..9bb23a97 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -154,12 +154,12 @@ pub enum SftpNum { impl SftpNum { fn is_request(&self) -> bool { // TODO SSH_FXP_EXTENDED - (2..=99).contains(*self as u8) + (2..=99).contains(&(*self as u8)) } fn is_response(&self) -> bool { // TODO SSH_FXP_EXTENDED_REPLY - (100..=199).contains(*self as u8) + (100..=199).contains(&(*self as u8)) } } From 31158f3b03912b1c4c5917c34ea99ea50e6e765c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:39:17 +1000 Subject: [PATCH 212/393] removing reduntant use in proto.rs --- sftp/src/proto.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 9bb23a97..5fe74f2d 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -8,7 +8,6 @@ use sunset::sshwire::{ use sunset::{Result, error}; use sunset_sshwire_derive::{SSHDecode, SSHEncode}; -use sunset::{Result, error}; // TODO is utf8 enough, or does this need to be an opaque binstring? #[derive(Debug)] From b9dfb55af24d869698a9da85117476ba4dbc33c1 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:47:42 +1000 Subject: [PATCH 213/393] Added variant to proto StatusCode --- sftp/src/proto.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 5fe74f2d..81b8710f 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -93,15 +93,25 @@ pub struct ReqId(pub u32); #[repr(u8)] #[allow(non_camel_case_types)] pub enum StatusCode { + #[sshwire(variant = "ssh_fx_ok")] SSH_FX_OK = 0, + #[sshwire(variant = "ssh_fx_eof")] SSH_FX_EOF = 1, + #[sshwire(variant = "ssh_fx_no_such_file")] SSH_FX_NO_SUCH_FILE = 2, + #[sshwire(variant = "ssh_fx_premission_denied")] SSH_FX_PERMISSION_DENIED = 3, + #[sshwire(variant = "ssh_fx_failure")] SSH_FX_FAILURE = 4, + #[sshwire(variant = "ssh_fx_bad_message")] SSH_FX_BAD_MESSAGE = 5, + #[sshwire(variant = "ssh_fx_no_connection")] SSH_FX_NO_CONNECTION = 6, + #[sshwire(variant = "ssh_fx_connection_lost")] SSH_FX_CONNECTION_LOST = 7, + #[sshwire(variant = "ssh_fx_unsupported")] SSH_FX_OP_UNSUPPORTED = 8, + #[sshwire(variant = "ssh_fx_unsupported")] Other(u8), } From 6cdd2616acaf09f6152a24651b23bbd8bf42cb4b Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 12:50:23 +1000 Subject: [PATCH 214/393] cleaning redundant comments the commit 7028e03cdb0dafdb3becd67bc4a96ad0f22a3a7d covers this --- sftp/src/proto.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 81b8710f..dacc3513 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -221,9 +221,6 @@ impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { // eg // MessageNumber::SSH_MSG_KEXINIT => Packet::KexInit( // ... - // Error: MessageNumber::$SSH_MESSAGE_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), - // ^^^^^^^^^^^^^^^^^ expected identifier - // Changed SSH_MESSAGE_NAME to SSH_FXP_NAME. I believe that this is the intended thing to do $( MessageNumber::$SSH_FXP_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), )* From a333362bcc2df78ef19f4ed783e47dced5adaea3 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 13:00:31 +1000 Subject: [PATCH 215/393] Removing some unused lifetimes that caused issues They can be added when needed --- sftp/src/proto.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index dacc3513..9e33482f 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -17,18 +17,17 @@ pub struct Filename<'a>(TextString<'a>); struct FileHandle<'a>(pub BinString<'a>); #[derive(Debug, SSHEncode, SSHDecode)] -pub struct InitVersion<'a> { +pub struct InitVersion { // No ReqId for SSH_FXP_INIT pub version: u32, // TODO variable number of ExtPair - pub _ext: &'a PhantomData<()>, } #[derive(Debug, SSHEncode, SSHDecode)] pub struct Open<'a> { pub filename: Filename<'a>, pub pflags: u32, - pub attrs: Attrs<'a>, + pub attrs: Attrs, } #[derive(Debug, SSHEncode, SSHDecode)] @@ -75,6 +74,7 @@ pub struct Data<'a> { pub struct Name<'a> { pub count: u32, // TODO repeat NameEntry + _pd: &'a PhantomData<()>, } #[derive(Debug, SSHEncode, SSHDecode)] @@ -83,7 +83,7 @@ pub struct NameEntry<'a> { /// longname is an undefined text line like "ls -l", /// SHOULD NOT be used. pub _longname: Filename<'a>, - pub attrs: Attrs<'a>, + pub attrs: Attrs, } #[derive(Debug, SSHEncode, SSHDecode, Clone, Copy)] @@ -122,7 +122,7 @@ pub struct ExtPair<'a> { } #[derive(Debug)] -pub struct Attrs<'a> { +pub struct Attrs { // flags: u32, defines used attributes pub size: Option, pub uid: Option, @@ -327,8 +327,8 @@ sftpmessages![ // Message number ranges are also used by Sftpnum::is_request and is_response. -(1, Init, InitVersion<'a>, SSH_FXP_INIT), -(2, Version, InitVersion<'a>, SSH_FXP_VERSION), +(1, Init, InitVersion, SSH_FXP_INIT), +(2, Version, InitVersion, SSH_FXP_VERSION), // Requests (3, Open, Open<'a>, SSH_FXP_OPEN), From 91c8c79f5d431b8bd4b946ae50b53e72362b909c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 13:06:22 +1000 Subject: [PATCH 216/393] Chasing SSHEncode,SSHDecode issues Adding the derive for Filename and Attrs --- sftp/src/proto.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 9e33482f..76f1dfde 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -10,7 +10,7 @@ use sunset::{Result, error}; use sunset_sshwire_derive::{SSHDecode, SSHEncode}; // TODO is utf8 enough, or does this need to be an opaque binstring? -#[derive(Debug)] +#[derive(Debug, SSHEncode, SSHDecode)] pub struct Filename<'a>(TextString<'a>); #[derive(Debug, SSHEncode, SSHDecode)] @@ -121,7 +121,7 @@ pub struct ExtPair<'a> { pub data: BinString<'a>, } -#[derive(Debug)] +#[derive(Debug, SSHEncode, SSHDecode)] pub struct Attrs { // flags: u32, defines used attributes pub size: Option, From b551f5edb333923330777bb9f4b8ef0e0bfa2144 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 13:56:58 +1000 Subject: [PATCH 217/393] Adding empty vanilla DirReply struct as ReadReply --- sftp/src/sftpserver.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 7b119849..0b249bad 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -36,3 +36,11 @@ pub struct ReadReply<'g, 'a> { impl<'g, 'a> ReadReply<'g, 'a> { pub async fn reply(self, data: &[u8]) {} } + +pub struct DirReply<'g, 'a> { + chan: ChanOut<'g, 'a>, +} + +impl<'g, 'a> DirReply<'g, 'a> { + pub async fn reply(self, data: &[u8]) {} +} From 164103988198a9f734223e18ffea9b23ed0184a2 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 14:02:56 +1000 Subject: [PATCH 218/393] Adding Vanilla ChanOut to finish the placeholders for Dir and Read Reply --- sftp/src/sftpserver.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 0b249bad..8a6ce16f 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,4 +1,5 @@ use crate::proto::{Attrs, StatusCode}; +use core::marker::PhantomData; pub type Result = core::result::Result; @@ -44,3 +45,9 @@ pub struct DirReply<'g, 'a> { impl<'g, 'a> DirReply<'g, 'a> { pub async fn reply(self, data: &[u8]) {} } + +// TODO: Implement correct Channel Out +pub struct ChanOut<'g, 'a> { + _phantom_g: PhantomData<&'g ()>, + _phantom_a: PhantomData<&'a ()>, +} From 4db4a87701bf145af2b040820cafeaad7981977f Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 14:06:27 +1000 Subject: [PATCH 219/393] Extra fix for SftpNum to avoid moving issues --- sftp/src/proto.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 76f1dfde..9bad04b4 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -149,7 +149,7 @@ macro_rules! sftpmessages { ) => { -#[derive(Debug)] +#[derive(Debug, Clone)] #[repr(u8)] #[allow(non_camel_case_types)] pub enum SftpNum { @@ -163,12 +163,12 @@ pub enum SftpNum { impl SftpNum { fn is_request(&self) -> bool { // TODO SSH_FXP_EXTENDED - (2..=99).contains(&(*self as u8)) + (2..=99).contains(&(self.clone() as u8)) } fn is_response(&self) -> bool { // TODO SSH_FXP_EXTENDED_REPLY - (100..=199).contains(&(*self as u8)) + (100..=199).contains(&(self.clone() as u8)) } } From a5834a9274ebe8af30f5eeac6e93a9a80abf6181 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 20 Aug 2025 15:11:50 +1000 Subject: [PATCH 220/393] WIP: Replacing sunset::Result with proto::Result Fixing issues with error handling is far from finished sunset::Result cannot take the new Errors definded in proto.rs therefore I have added error conversion for sftp errors to Sunset global error. --- sftp/src/proto.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 9bad04b4..c6c43336 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,11 +1,12 @@ use core::marker::PhantomData; +use sunset::error; +use sunset::error::Error as SunsetError; use sunset::packets::{MessageNumber, Packet}; use sunset::sshwire::{ BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, WireResult, }; -use sunset::{Result, error}; use sunset_sshwire_derive::{SSHDecode, SSHEncode}; @@ -134,10 +135,25 @@ pub struct Attrs { // TODO extensions } -enum Error { +#[derive(Debug)] +pub enum Error { UnknownPacket { number: u8 }, } +pub type Result = core::result::Result; + +impl From for SunsetError { + fn from(error: Error) -> SunsetError { + SunsetError::Custom { + msg: match error { + Error::UnknownPacket { number } => { + format_args!("Unknown SFTP packet: {}", number) + } + }, + } + } +} + macro_rules! sftpmessages { ( $( ( $message_num:literal, From d193e090357af2cb513eefdf79cc4b8e9a99aaff Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 11:07:58 +1000 Subject: [PATCH 221/393] commenting out sftpmessages macro rules to focus on the foundational SFTP data types --- sftp/src/proto.rs | 407 +++++++++++++++++++++++----------------------- 1 file changed, 203 insertions(+), 204 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index c6c43336..2fee115a 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -154,207 +154,206 @@ impl From for SunsetError { } } -macro_rules! sftpmessages { - ( - $( ( $message_num:literal, - $SpecificPacketVariant:ident, - $SpecificPacketType:ty, - $SSH_FXP_NAME:ident - ), - )* - ) => { - - -#[derive(Debug, Clone)] -#[repr(u8)] -#[allow(non_camel_case_types)] -pub enum SftpNum { - // variants are eg - // SSH_FXP_OPEN = 3, - $( - $SSH_FXP_NAME = $message_num, - )* -} - -impl SftpNum { - fn is_request(&self) -> bool { - // TODO SSH_FXP_EXTENDED - (2..=99).contains(&(self.clone() as u8)) - } - - fn is_response(&self) -> bool { - // TODO SSH_FXP_EXTENDED_REPLY - (100..=199).contains(&(self.clone() as u8)) - } -} - -impl TryFrom for SftpNum { - type Error = Error; - fn try_from(v: u8) -> Result { - match v { - // eg - // 3 => Ok(SftpNum::SSH_FXP_OPEN) - $( - $message_num => Ok(SftpNum::$SSH_FXP_NAME), - )* - _ => { - Err(Error::UnknownPacket { number: v }) - } - } - } -} - -impl SSHEncode for SftpPacket<'_> { - fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { - let t = self.message_num() as u8; - t.enc(s)?; - match self { - // eg - // Packet::KexInit(p) => { - // ... - $( - Packet::$SpecificPacketVariant(p) => { - p.enc(s)? - } - )* - }; - Ok(()) - } -} - -impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { - fn dec(s: &mut S) -> WireResult - where S: SSHSource<'de> { - let msg_num = u8::dec(s)?; - let ty = MessageNumber::try_from(msg_num); - let ty = match ty { - Ok(t) => t, - Err(_) => return Err(WireError::UnknownPacket { number: msg_num }) - }; - - // Decode based on the message number - let p = match ty { - // eg - // MessageNumber::SSH_MSG_KEXINIT => Packet::KexInit( - // ... - $( - MessageNumber::$SSH_FXP_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), - )* - }; - Ok(p) - } -} - -/// Top level SSH packet enum -#[derive(Debug)] -pub enum SftpPacket<'a> { - // eg Open(Open<'a>), - $( - $SpecificPacketVariant($SpecificPacketType), - )* -} - -impl<'a> SftpPacket<'a> { - pub fn sftp_num(&self) -> SftpNum { - match self { - // eg - // SftpPacket::Open(_) => { - // .. - $( - SftpPacket::$SpecificPacketVariant(_) => { - MessageNumber::$SSH_FXP_NAME - } - )* - } - } - - /// Encode a request. - /// - /// Used by a SFTP client. Does not include the length field. - pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> { - if !self.sftp_num().is_request() { - return Err(Error::bug()) - } - - // packet type - self.sftp_num().enc(s)?; - // request ID - id.0.enc(s)?; - // contents - self.enc(s) - } - - /// Decode a response. - /// - /// Used by a SFTP client. Does not include the length field. - pub fn decode_response(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> { - let num = SftpNum::try_from(u8::dec(s)?)?; - - if !num.is_response() { - return error::SSHProto.fail(); - } - - let id = ReqId(u32::dec(s)?); - Ok((id, Self::dec(s))) - } - - /// Decode a request. - /// - /// Used by a SFTP server. Does not include the length field. - pub fn decode_request(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> { - let num = SftpNum::try_from(u8::dec(s)?)?; - - if !num.is_request() { - return error::SSHProto.fail(); - } - - let id = ReqId(u32::dec(s)?); - Ok((id, Self::dec(s))) - } - - /// Encode a response. - /// - /// Used by a SFTP server. Does not include the length field. - pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> { - if !self.sftp_num().is_response() { - return Err(Error::bug()) - } - - // packet type - self.sftp_num().enc(s)?; - // request ID - id.0.enc(s)?; - // contents - self.enc(s) - } -} - -$( -impl<'a> From<$SpecificPacketType> for SftpPacket<'a> { - fn from(s: $SpecificPacketType) -> SftpPacket<'a> { - SftpPacket::$SpecificPacketVariant(s) - } -} -)* - -} } // macro - -sftpmessages![ - -// Message number ranges are also used by Sftpnum::is_request and is_response. - -(1, Init, InitVersion, SSH_FXP_INIT), -(2, Version, InitVersion, SSH_FXP_VERSION), - -// Requests -(3, Open, Open<'a>, SSH_FXP_OPEN), -(4, Close, Close<'a>, SSH_FXP_CLOSE), -(5, Read, Read<'a>, SSH_FXP_READ), - -// Responses -(101, Status, Status<'a>, SSH_FXP_STATUS), -(102, Handle, Handle<'a>, SSH_FXP_HANDLE), -(103, Data, Data<'a>, SSH_FXP_DATA), -(104, Name, Name<'a>, SSH_FXP_NAME), - -]; +// macro_rules! sftpmessages { +// ( +// $( ( $message_num:literal, +// $SpecificPacketVariant:ident, +// $SpecificPacketType:ty, +// $SSH_FXP_NAME:ident +// ), +// )* +// ) => { + +// #[derive(Debug, Clone)] +// #[repr(u8)] +// #[allow(non_camel_case_types)] +// pub enum SftpNum { +// // variants are eg +// // SSH_FXP_OPEN = 3, +// $( +// $SSH_FXP_NAME = $message_num, +// )* +// } + +// impl SftpNum { +// fn is_request(&self) -> bool { +// // TODO SSH_FXP_EXTENDED +// (2..=99).contains(&(self.clone() as u8)) +// } + +// fn is_response(&self) -> bool { +// // TODO SSH_FXP_EXTENDED_REPLY +// (100..=199).contains(&(self.clone() as u8)) +// } +// } + +// impl TryFrom for SftpNum { +// type Error = Error; +// fn try_from(v: u8) -> Result { +// match v { +// // eg +// // 3 => Ok(SftpNum::SSH_FXP_OPEN) +// $( +// $message_num => Ok(SftpNum::$SSH_FXP_NAME), +// )* +// _ => { +// Err(Error::UnknownPacket { number: v }) +// } +// } +// } +// } + +// impl SSHEncode for SftpPacket<'_> { +// fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { +// let t = self.message_num() as u8; +// t.enc(s)?; +// match self { +// // eg +// // Packet::KexInit(p) => { +// // ... +// $( +// Packet::$SpecificPacketVariant(p) => { +// p.enc(s)? +// } +// )* +// }; +// Ok(()) +// } +// } + +// impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { +// fn dec(s: &mut S) -> WireResult +// where S: SSHSource<'de> { +// let msg_num = u8::dec(s)?; +// let ty = MessageNumber::try_from(msg_num); +// let ty = match ty { +// Ok(t) => t, +// Err(_) => return Err(WireError::UnknownPacket { number: msg_num }) +// }; + +// // Decode based on the message number +// let p = match ty { +// // eg +// // MessageNumber::SSH_MSG_KEXINIT => Packet::KexInit( +// // ... +// $( +// MessageNumber::$SSH_FXP_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), +// )* +// }; +// Ok(p) +// } +// } + +// /// Top level SSH packet enum +// #[derive(Debug)] +// pub enum SftpPacket<'a> { +// // eg Open(Open<'a>), +// $( +// $SpecificPacketVariant($SpecificPacketType), +// )* +// } + +// impl<'a> SftpPacket<'a> { +// pub fn sftp_num(&self) -> SftpNum { +// match self { +// // eg +// // SftpPacket::Open(_) => { +// // .. +// $( +// SftpPacket::$SpecificPacketVariant(_) => { +// MessageNumber::$SSH_FXP_NAME +// } +// )* +// } +// } + +// /// Encode a request. +// /// +// /// Used by a SFTP client. Does not include the length field. +// pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> { +// if !self.sftp_num().is_request() { +// return Err(Error::bug()) +// } + +// // packet type +// self.sftp_num().enc(s)?; +// // request ID +// id.0.enc(s)?; +// // contents +// self.enc(s) +// } + +// /// Decode a response. +// /// +// /// Used by a SFTP client. Does not include the length field. +// pub fn decode_response(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> { +// let num = SftpNum::try_from(u8::dec(s)?)?; + +// if !num.is_response() { +// return error::SSHProto.fail(); +// } + +// let id = ReqId(u32::dec(s)?); +// Ok((id, Self::dec(s))) +// } + +// /// Decode a request. +// /// +// /// Used by a SFTP server. Does not include the length field. +// pub fn decode_request(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> { +// let num = SftpNum::try_from(u8::dec(s)?)?; + +// if !num.is_request() { +// return error::SSHProto.fail(); +// } + +// let id = ReqId(u32::dec(s)?); +// Ok((id, Self::dec(s))) +// } + +// /// Encode a response. +// /// +// /// Used by a SFTP server. Does not include the length field. +// pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> { +// if !self.sftp_num().is_response() { +// return Err(Error::bug()) +// } + +// // packet type +// self.sftp_num().enc(s)?; +// // request ID +// id.0.enc(s)?; +// // contents +// self.enc(s) +// } +// } + +// $( +// impl<'a> From<$SpecificPacketType> for SftpPacket<'a> { +// fn from(s: $SpecificPacketType) -> SftpPacket<'a> { +// SftpPacket::$SpecificPacketVariant(s) +// } +// } +// )* + +// } } // macro + +// sftpmessages![ + +// // Message number ranges are also used by Sftpnum::is_request and is_response. + +// (1, Init, InitVersion, SSH_FXP_INIT), +// (2, Version, InitVersion, SSH_FXP_VERSION), + +// // Requests +// (3, Open, Open<'a>, SSH_FXP_OPEN), +// (4, Close, Close<'a>, SSH_FXP_CLOSE), +// (5, Read, Read<'a>, SSH_FXP_READ), + +// // Responses +// (101, Status, Status<'a>, SSH_FXP_STATUS), +// (102, Handle, Handle<'a>, SSH_FXP_HANDLE), +// (103, Data, Data<'a>, SSH_FXP_DATA), +// (104, Name, Name<'a>, SSH_FXP_NAME), + +// ]; From 0ea931ee4c9a995b949464da4cc07b37ee65a598 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 11:11:34 +1000 Subject: [PATCH 222/393] commenting out Result and ane extra SunsetError implementation too --- sftp/src/proto.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 2fee115a..10d9ad2e 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -140,19 +140,19 @@ pub enum Error { UnknownPacket { number: u8 }, } -pub type Result = core::result::Result; - -impl From for SunsetError { - fn from(error: Error) -> SunsetError { - SunsetError::Custom { - msg: match error { - Error::UnknownPacket { number } => { - format_args!("Unknown SFTP packet: {}", number) - } - }, - } - } -} +// pub type Result = core::result::Result; + +// impl From for SunsetError { +// fn from(error: Error) -> SunsetError { +// SunsetError::Custom { +// msg: match error { +// Error::UnknownPacket { number } => { +// format_args!("Unknown SFTP packet: {}", number) +// } +// }, +// } +// } +// } // macro_rules! sftpmessages { // ( From 164c9ee62caeca5efbdcc0b32a7c1eed51d5c492 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 11:55:33 +1000 Subject: [PATCH 223/393] Removing phantomData for the moment to avoid SSHEncode/Decode issues --- sftp/src/proto.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 10d9ad2e..25722a3b 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,5 +1,3 @@ -use core::marker::PhantomData; - use sunset::error; use sunset::error::Error as SunsetError; use sunset::packets::{MessageNumber, Packet}; @@ -52,12 +50,12 @@ pub struct Write<'a> { // Responses -#[derive(Debug, SSHEncode, SSHDecode)] -pub struct Status<'a> { - pub code: StatusCode, - pub message: TextString<'a>, - pub lang: TextString<'a>, -} +// #[derive(Debug, SSHEncode, SSHDecode)] +// pub struct Status<'a> { +// pub code: StatusCode, +// pub message: TextString<'a>, +// pub lang: TextString<'a>, +// } #[derive(Debug, SSHEncode, SSHDecode)] pub struct Handle<'a> { @@ -72,10 +70,9 @@ pub struct Data<'a> { } #[derive(Debug, SSHEncode, SSHDecode)] -pub struct Name<'a> { +pub struct Name { pub count: u32, // TODO repeat NameEntry - _pd: &'a PhantomData<()>, } #[derive(Debug, SSHEncode, SSHDecode)] From 59a2f9d5bccd969c3ec2083c70e2e3728aac96e6 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 11:58:19 +1000 Subject: [PATCH 224/393] Implementing SSHEncode/Decode for Attrs To do so, I introduced the method flags(), and the enum AttrsFlags to serialise and deserialise the flags --- sftp/src/proto.rs | 116 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 4 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 25722a3b..7f7aeef7 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -119,7 +119,7 @@ pub struct ExtPair<'a> { pub data: BinString<'a>, } -#[derive(Debug, SSHEncode, SSHDecode)] +#[derive(Debug, Default)] pub struct Attrs { // flags: u32, defines used attributes pub size: Option, @@ -132,11 +132,119 @@ pub struct Attrs { // TODO extensions } -#[derive(Debug)] -pub enum Error { - UnknownPacket { number: u8 }, +#[repr(u32)] +#[allow(non_camel_case_types)] +pub enum AttrsFlags { + SSH_FILEXFER_ATTR_SIZE = 0x01, + SSH_FILEXFER_ATTR_UIDGID = 0x02, + SSH_FILEXFER_ATTR_PERMISSIONS = 0x04, + SSH_FILEXFER_ATTR_ACMODTIME = 0x08, + SSH_FILEXFER_ATTR_EXTENDED = 0x80000000, +} +impl core::ops::AddAssign for u32 { + fn add_assign(&mut self, other: AttrsFlags) { + *self |= other as u32; + } +} + +impl core::ops::BitAnd for u32 { + type Output = u32; + + fn bitand(self, rhs: AttrsFlags) -> Self::Output { + self & rhs as u32 + } +} + +impl Attrs { + pub fn flags(&self) -> u32 { + let mut flags: u32 = 0; + if self.size.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_SIZE + } + if self.uid.is_some() || self.gid.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_UIDGID + } + if self.permissions.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_PERMISSIONS + } + if self.atime.is_some() || self.mtime.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_ACMODTIME + } + // TODO: Implement extensions + // if self.ext_count.is_some() { + // flags += AttrsFlags::SSH_FILEXFER_ATTR_EXTENDED + // } + + flags + } } +impl SSHEncode for Attrs { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + self.flags().enc(s)?; + + // IMPORTANT: Order matters in the encoding/decoding since it will be interpreted together with the flags + if let Some(value) = self.size.as_ref() { + value.enc(s)? + } + if let Some(value) = self.uid.as_ref() { + value.enc(s)? + } + if let Some(value) = self.gid.as_ref() { + value.enc(s)? + } + if let Some(value) = self.permissions.as_ref() { + value.enc(s)? + } + if let Some(value) = self.atime.as_ref() { + value.enc(s)? + } + if let Some(value) = self.mtime.as_ref() { + value.enc(s)? + } + // TODO: Implement extensions + // if let Some(value) = self.ext_count.as_ref() { value.enc(s)? } + + Ok(()) + } +} + +impl<'de> SSHDecode<'de> for Attrs { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + let mut attrs = Attrs::default(); + let flags = u32::dec(s)? as u32; + if flags & AttrsFlags::SSH_FILEXFER_ATTR_SIZE != 0 { + attrs.size = Some(u64::dec(s)?); + } + if flags & AttrsFlags::SSH_FILEXFER_ATTR_UIDGID != 0 { + attrs.uid = Some(u32::dec(s)?); + attrs.gid = Some(u32::dec(s)?); + } + if flags & AttrsFlags::SSH_FILEXFER_ATTR_PERMISSIONS != 0 { + attrs.permissions = Some(u32::dec(s)?); + } + if flags & AttrsFlags::SSH_FILEXFER_ATTR_ACMODTIME != 0 { + attrs.atime = Some(u32::dec(s)?); + attrs.mtime = Some(u32::dec(s)?); + } + // TODO: Implement extensions + // if flags & AttrsFlags::SSH_FILEXFER_ATTR_EXTENDED != 0{ + + // todo!("Not implemented"); + // } + + Ok(attrs) + } +} + +// #[derive(Debug)] +// pub enum Error { +// UnknownPacket { number: u8 }, +// } + // pub type Result = core::result::Result; // impl From for SunsetError { From 5c8fac1da679e79a1f56640bde70111e08d14b7c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 11:59:43 +1000 Subject: [PATCH 225/393] Added a response variant "ResponseAttributes" to complete all server responses SFTP packets Feel free to rename it --- sftp/src/proto.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 7f7aeef7..d6fb0af4 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -84,6 +84,13 @@ pub struct NameEntry<'a> { pub attrs: Attrs, } +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct ResponseAttributes { + pub attrs: Attrs, +} + +// Requests/Responses data types + #[derive(Debug, SSHEncode, SSHDecode, Clone, Copy)] pub struct ReqId(pub u32); From dc86dc9cee07e66cf2c8d4c7a3e006071e75bc2c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 12:06:04 +1000 Subject: [PATCH 226/393] Fixed typo and change other variant string to ssh_fx_other --- sftp/src/proto.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index d6fb0af4..0e5e6484 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -104,7 +104,7 @@ pub enum StatusCode { SSH_FX_EOF = 1, #[sshwire(variant = "ssh_fx_no_such_file")] SSH_FX_NO_SUCH_FILE = 2, - #[sshwire(variant = "ssh_fx_premission_denied")] + #[sshwire(variant = "ssh_fx_permission_denied")] SSH_FX_PERMISSION_DENIED = 3, #[sshwire(variant = "ssh_fx_failure")] SSH_FX_FAILURE = 4, @@ -116,7 +116,7 @@ pub enum StatusCode { SSH_FX_CONNECTION_LOST = 7, #[sshwire(variant = "ssh_fx_unsupported")] SSH_FX_OP_UNSUPPORTED = 8, - #[sshwire(variant = "ssh_fx_unsupported")] + #[sshwire(variant = "ssh_fx_other")] Other(u8), } From 5ffc2d2b841cf299661211194773c7cf17025c8a Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 15:34:36 +1000 Subject: [PATCH 227/393] isolating proto.rs issues. Commenting out sftpserver --- sftp/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 4c2f3ffe..0368d938 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,2 +1,2 @@ mod proto; -mod sftpserver; +// mod sftpserver; From a44f70c9c3430f8470a0d51985747591daa4511c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 15:40:28 +1000 Subject: [PATCH 228/393] Modifying packets.rs ParseContext for allowing proto.rs StatusCode to use #[sshwire(unknown)] Without changing some visiblitities, the code generated by SSHDecode (SSHDecodeEnum) for proto.rs would try accessing s.ctx().seen_unknown and Unknown::new() throwing errors. Am I doing using SSHDecode wrong? --- src/packets.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index 6ffe97d2..a96c9a93 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -841,7 +841,7 @@ pub struct DirectTcpip<'a> { pub struct Unknown<'a>(pub &'a [u8]); impl<'a> Unknown<'a> { - fn new(u: &'a [u8]) -> Self { + pub fn new(u: &'a [u8]) -> Self { let u = Unknown(u); trace!("saw unknown variant \"{u}\""); u @@ -882,7 +882,7 @@ pub struct ParseContext { // Set to true if an unknown variant is encountered. // Packet length checks should be omitted in that case. - pub(crate) seen_unknown: bool, + pub seen_unknown: bool, } impl ParseContext { From f9d637eded2c94ecbce17a8ab3ad389debca41cb Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 22 Aug 2025 15:46:16 +1000 Subject: [PATCH 229/393] proto.rs StatusCode SSHEncode expanded version seems incorrect. I made a StatusCode version with lifetime (to be used by Other) and StatusCode SSHEncode autogenerated enc() function (L584) looks wrong since it is not performing any ::sunset::sshwire::SSHEncode::enc() Am I using SSHEncode wrong for this enum? --- sftp/out/cargo-expand-sftp.rs | 829 ++++++++++++++++++++++++++++++++++ sftp/src/proto.rs | 9 +- 2 files changed, 834 insertions(+), 4 deletions(-) create mode 100644 sftp/out/cargo-expand-sftp.rs diff --git a/sftp/out/cargo-expand-sftp.rs b/sftp/out/cargo-expand-sftp.rs new file mode 100644 index 00000000..b2766361 --- /dev/null +++ b/sftp/out/cargo-expand-sftp.rs @@ -0,0 +1,829 @@ +#![feature(prelude_import)] +#[macro_use] +extern crate std; +#[prelude_import] +use std::prelude::rust_2024::*; +mod proto { + use sunset::error; + use sunset::error::Error as SunsetError; + use sunset::packets::{MessageNumber, Packet, Unknown}; + use sunset::sshwire::{ + BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, + WireResult, + }; + use sunset_sshwire_derive::{SSHDecode, SSHEncode}; + pub struct Filename<'a>(TextString<'a>); + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Filename<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Filename", &&self.0) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Filename<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.0, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Filename<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) + } + } + struct FileHandle<'a>(pub BinString<'a>); + #[automatically_derived] + impl<'a> ::core::fmt::Debug for FileHandle<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_tuple_field1_finish(f, "FileHandle", &&self.0) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for FileHandle<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.0, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for FileHandle<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) + } + } + pub struct InitVersion { + pub version: u32, + } + #[automatically_derived] + impl ::core::fmt::Debug for InitVersion { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "InitVersion", + "version", + &&self.version, + ) + } + } + impl ::sunset::sshwire::SSHEncode for InitVersion { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.version, s)?; + Ok(()) + } + } + impl<'de> ::sunset::sshwire::SSHDecode<'de> for InitVersion { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_version = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { version: field_version }) + } + } + pub struct Open<'a> { + pub filename: Filename<'a>, + pub pflags: u32, + pub attrs: Attrs, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Open<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field3_finish( + f, + "Open", + "filename", + &self.filename, + "pflags", + &self.pflags, + "attrs", + &&self.attrs, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Open<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.filename, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.pflags, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.attrs, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Open<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_filename = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_pflags = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_attrs = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { + filename: field_filename, + pflags: field_pflags, + attrs: field_attrs, + }) + } + } + pub struct Close<'a> { + pub handle: FileHandle<'a>, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Close<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "Close", + "handle", + &&self.handle, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Close<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Close<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { handle: field_handle }) + } + } + pub struct Read<'a> { + pub handle: FileHandle<'a>, + pub offset: u64, + pub len: u32, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Read<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field3_finish( + f, + "Read", + "handle", + &self.handle, + "offset", + &self.offset, + "len", + &&self.len, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Read<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.offset, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.len, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Read<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_offset = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_len = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { + handle: field_handle, + offset: field_offset, + len: field_len, + }) + } + } + pub struct Write<'a> { + pub handle: FileHandle<'a>, + pub offset: u64, + pub data: BinString<'a>, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Write<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field3_finish( + f, + "Write", + "handle", + &self.handle, + "offset", + &self.offset, + "data", + &&self.data, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Write<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.offset, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.data, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Write<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_offset = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_data = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { + handle: field_handle, + offset: field_offset, + data: field_data, + }) + } + } + pub struct Handle<'a> { + pub handle: FileHandle<'a>, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Handle<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "Handle", + "handle", + &&self.handle, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Handle<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Handle<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { handle: field_handle }) + } + } + pub struct Data<'a> { + pub handle: FileHandle<'a>, + pub offset: u64, + pub data: BinString<'a>, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Data<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field3_finish( + f, + "Data", + "handle", + &self.handle, + "offset", + &self.offset, + "data", + &&self.data, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Data<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.offset, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.data, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Data<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_offset = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_data = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { + handle: field_handle, + offset: field_offset, + data: field_data, + }) + } + } + pub struct Name { + pub count: u32, + } + #[automatically_derived] + impl ::core::fmt::Debug for Name { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "Name", + "count", + &&self.count, + ) + } + } + impl ::sunset::sshwire::SSHEncode for Name { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.count, s)?; + Ok(()) + } + } + impl<'de> ::sunset::sshwire::SSHDecode<'de> for Name { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_count = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { count: field_count }) + } + } + pub struct NameEntry<'a> { + pub filename: Filename<'a>, + /// longname is an undefined text line like "ls -l", + /// SHOULD NOT be used. + pub _longname: Filename<'a>, + pub attrs: Attrs, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for NameEntry<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field3_finish( + f, + "NameEntry", + "filename", + &self.filename, + "_longname", + &self._longname, + "attrs", + &&self.attrs, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for NameEntry<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.filename, s)?; + ::sunset::sshwire::SSHEncode::enc(&self._longname, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.attrs, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for NameEntry<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_filename = ::sunset::sshwire::SSHDecode::dec(s)?; + let field__longname = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_attrs = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { + filename: field_filename, + _longname: field__longname, + attrs: field_attrs, + }) + } + } + pub struct ResponseAttributes { + pub attrs: Attrs, + } + #[automatically_derived] + impl ::core::fmt::Debug for ResponseAttributes { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "ResponseAttributes", + "attrs", + &&self.attrs, + ) + } + } + impl ::sunset::sshwire::SSHEncode for ResponseAttributes { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.attrs, s)?; + Ok(()) + } + } + impl<'de> ::sunset::sshwire::SSHDecode<'de> for ResponseAttributes { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_attrs = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { attrs: field_attrs }) + } + } + pub struct ReqId(pub u32); + #[automatically_derived] + impl ::core::fmt::Debug for ReqId { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_tuple_field1_finish(f, "ReqId", &&self.0) + } + } + impl ::sunset::sshwire::SSHEncode for ReqId { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.0, s)?; + Ok(()) + } + } + impl<'de> ::sunset::sshwire::SSHDecode<'de> for ReqId { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) + } + } + #[automatically_derived] + impl ::core::clone::Clone for ReqId { + #[inline] + fn clone(&self) -> ReqId { + let _: ::core::clone::AssertParamIsClone; + *self + } + } + #[automatically_derived] + impl ::core::marker::Copy for ReqId {} + #[repr(u8)] + #[allow(non_camel_case_types)] + pub enum StatusCode<'a> { + #[sshwire(variant = "ssh_fx_ok")] + SSH_FX_OK = 0, + #[sshwire(variant = "ssh_fx_eof")] + SSH_FX_EOF = 1, + #[sshwire(variant = "ssh_fx_no_such_file")] + SSH_FX_NO_SUCH_FILE = 2, + #[sshwire(variant = "ssh_fx_permission_denied")] + SSH_FX_PERMISSION_DENIED = 3, + #[sshwire(variant = "ssh_fx_failure")] + SSH_FX_FAILURE = 4, + #[sshwire(variant = "ssh_fx_bad_message")] + SSH_FX_BAD_MESSAGE = 5, + #[sshwire(variant = "ssh_fx_no_connection")] + SSH_FX_NO_CONNECTION = 6, + #[sshwire(variant = "ssh_fx_connection_lost")] + SSH_FX_CONNECTION_LOST = 7, + #[sshwire(variant = "ssh_fx_unsupported")] + SSH_FX_OP_UNSUPPORTED = 8, + #[sshwire(unknown)] + Other(Unknown<'a>), + } + #[automatically_derived] + #[allow(non_camel_case_types)] + impl<'a> ::core::fmt::Debug for StatusCode<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + StatusCode::SSH_FX_OK => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_OK") + } + StatusCode::SSH_FX_EOF => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_EOF") + } + StatusCode::SSH_FX_NO_SUCH_FILE => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_NO_SUCH_FILE") + } + StatusCode::SSH_FX_PERMISSION_DENIED => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_PERMISSION_DENIED") + } + StatusCode::SSH_FX_FAILURE => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_FAILURE") + } + StatusCode::SSH_FX_BAD_MESSAGE => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_BAD_MESSAGE") + } + StatusCode::SSH_FX_NO_CONNECTION => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_NO_CONNECTION") + } + StatusCode::SSH_FX_CONNECTION_LOST => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_CONNECTION_LOST") + } + StatusCode::SSH_FX_OP_UNSUPPORTED => { + ::core::fmt::Formatter::write_str(f, "SSH_FX_OP_UNSUPPORTED") + } + StatusCode::Other(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Other", + &__self_0, + ) + } + } + } + } + impl<'a> ::sunset::sshwire::SSHEncode for StatusCode<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + match *self { + Self::SSH_FX_OK => {} + Self::SSH_FX_EOF => {} + Self::SSH_FX_NO_SUCH_FILE => {} + Self::SSH_FX_PERMISSION_DENIED => {} + Self::SSH_FX_FAILURE => {} + Self::SSH_FX_BAD_MESSAGE => {} + Self::SSH_FX_NO_CONNECTION => {} + Self::SSH_FX_CONNECTION_LOST => {} + Self::SSH_FX_OP_UNSUPPORTED => {} + Self::Other(ref i) => { + return Err(::sunset::sshwire::WireError::UnknownVariant); + } + } + #[allow(unreachable_code)] Ok(()) + } + } + impl<'a> ::sunset::sshwire::SSHEncodeEnum for StatusCode<'a> { + fn variant_name(&self) -> ::sunset::sshwire::WireResult<&'static str> { + let r = match self { + Self::SSH_FX_OK => "ssh_fx_ok", + Self::SSH_FX_EOF => "ssh_fx_eof", + Self::SSH_FX_NO_SUCH_FILE => "ssh_fx_no_such_file", + Self::SSH_FX_PERMISSION_DENIED => "ssh_fx_permission_denied", + Self::SSH_FX_FAILURE => "ssh_fx_failure", + Self::SSH_FX_BAD_MESSAGE => "ssh_fx_bad_message", + Self::SSH_FX_NO_CONNECTION => "ssh_fx_no_connection", + Self::SSH_FX_CONNECTION_LOST => "ssh_fx_connection_lost", + Self::SSH_FX_OP_UNSUPPORTED => "ssh_fx_unsupported", + Self::Other(_) => { + return Err(::sunset::sshwire::WireError::UnknownVariant); + } + }; + #[allow(unreachable_code)] Ok(r) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecodeEnum<'de> for StatusCode<'a> + where + 'de: 'a, + { + fn dec_enum>( + s: &mut S, + variant: &'de [u8], + ) -> ::sunset::sshwire::WireResult { + let var_str = ::sunset::sshwire::try_as_ascii_str(variant).ok(); + let r = match var_str { + Some("ssh_fx_ok") => Self::SSH_FX_OK, + Some("ssh_fx_eof") => Self::SSH_FX_EOF, + Some("ssh_fx_no_such_file") => Self::SSH_FX_NO_SUCH_FILE, + Some("ssh_fx_permission_denied") => Self::SSH_FX_PERMISSION_DENIED, + Some("ssh_fx_failure") => Self::SSH_FX_FAILURE, + Some("ssh_fx_bad_message") => Self::SSH_FX_BAD_MESSAGE, + Some("ssh_fx_no_connection") => Self::SSH_FX_NO_CONNECTION, + Some("ssh_fx_connection_lost") => Self::SSH_FX_CONNECTION_LOST, + Some("ssh_fx_unsupported") => Self::SSH_FX_OP_UNSUPPORTED, + _ => { + s.ctx().seen_unknown = true; + Self::Other(Unknown::new(variant)) + } + }; + Ok(r) + } + } + pub struct ExtPair<'a> { + pub name: &'a str, + pub data: BinString<'a>, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for ExtPair<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field2_finish( + f, + "ExtPair", + "name", + &self.name, + "data", + &&self.data, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for ExtPair<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.name, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.data, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for ExtPair<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_name = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_data = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { + name: field_name, + data: field_data, + }) + } + } + pub struct Attrs { + pub size: Option, + pub uid: Option, + pub gid: Option, + pub permissions: Option, + pub atime: Option, + pub mtime: Option, + pub ext_count: Option, + } + #[automatically_derived] + impl ::core::fmt::Debug for Attrs { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + let names: &'static _ = &[ + "size", + "uid", + "gid", + "permissions", + "atime", + "mtime", + "ext_count", + ]; + let values: &[&dyn ::core::fmt::Debug] = &[ + &self.size, + &self.uid, + &self.gid, + &self.permissions, + &self.atime, + &self.mtime, + &&self.ext_count, + ]; + ::core::fmt::Formatter::debug_struct_fields_finish(f, "Attrs", names, values) + } + } + #[automatically_derived] + impl ::core::default::Default for Attrs { + #[inline] + fn default() -> Attrs { + Attrs { + size: ::core::default::Default::default(), + uid: ::core::default::Default::default(), + gid: ::core::default::Default::default(), + permissions: ::core::default::Default::default(), + atime: ::core::default::Default::default(), + mtime: ::core::default::Default::default(), + ext_count: ::core::default::Default::default(), + } + } + } + #[repr(u32)] + #[allow(non_camel_case_types)] + pub enum AttrsFlags { + SSH_FILEXFER_ATTR_SIZE = 0x01, + SSH_FILEXFER_ATTR_UIDGID = 0x02, + SSH_FILEXFER_ATTR_PERMISSIONS = 0x04, + SSH_FILEXFER_ATTR_ACMODTIME = 0x08, + SSH_FILEXFER_ATTR_EXTENDED = 0x80000000, + } + impl core::ops::AddAssign for u32 { + fn add_assign(&mut self, other: AttrsFlags) { + *self |= other as u32; + } + } + impl core::ops::BitAnd for u32 { + type Output = u32; + fn bitand(self, rhs: AttrsFlags) -> Self::Output { + self & rhs as u32 + } + } + impl Attrs { + pub fn flags(&self) -> u32 { + let mut flags: u32 = 0; + if self.size.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_SIZE; + } + if self.uid.is_some() || self.gid.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_UIDGID; + } + if self.permissions.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_PERMISSIONS; + } + if self.atime.is_some() || self.mtime.is_some() { + flags += AttrsFlags::SSH_FILEXFER_ATTR_ACMODTIME; + } + flags + } + } + impl SSHEncode for Attrs { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + self.flags().enc(s)?; + if let Some(value) = self.size.as_ref() { + value.enc(s)? + } + if let Some(value) = self.uid.as_ref() { + value.enc(s)? + } + if let Some(value) = self.gid.as_ref() { + value.enc(s)? + } + if let Some(value) = self.permissions.as_ref() { + value.enc(s)? + } + if let Some(value) = self.atime.as_ref() { + value.enc(s)? + } + if let Some(value) = self.mtime.as_ref() { + value.enc(s)? + } + Ok(()) + } + } + impl<'de> SSHDecode<'de> for Attrs { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + let mut attrs = Attrs::default(); + let flags = u32::dec(s)? as u32; + if flags & AttrsFlags::SSH_FILEXFER_ATTR_SIZE != 0 { + attrs.size = Some(u64::dec(s)?); + } + if flags & AttrsFlags::SSH_FILEXFER_ATTR_UIDGID != 0 { + attrs.uid = Some(u32::dec(s)?); + attrs.gid = Some(u32::dec(s)?); + } + if flags & AttrsFlags::SSH_FILEXFER_ATTR_PERMISSIONS != 0 { + attrs.permissions = Some(u32::dec(s)?); + } + if flags & AttrsFlags::SSH_FILEXFER_ATTR_ACMODTIME != 0 { + attrs.atime = Some(u32::dec(s)?); + attrs.mtime = Some(u32::dec(s)?); + } + Ok(attrs) + } + } +} diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 0e5e6484..e44740e0 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,6 +1,6 @@ use sunset::error; use sunset::error::Error as SunsetError; -use sunset::packets::{MessageNumber, Packet}; +use sunset::packets::{MessageNumber, Packet, Unknown}; use sunset::sshwire::{ BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, WireResult, @@ -97,7 +97,7 @@ pub struct ReqId(pub u32); #[derive(Debug, SSHEncode, SSHDecode)] #[repr(u8)] #[allow(non_camel_case_types)] -pub enum StatusCode { +pub enum StatusCode<'a> { #[sshwire(variant = "ssh_fx_ok")] SSH_FX_OK = 0, #[sshwire(variant = "ssh_fx_eof")] @@ -116,8 +116,9 @@ pub enum StatusCode { SSH_FX_CONNECTION_LOST = 7, #[sshwire(variant = "ssh_fx_unsupported")] SSH_FX_OP_UNSUPPORTED = 8, - #[sshwire(variant = "ssh_fx_other")] - Other(u8), + // #[sshwire(variant = "ssh_fx_other")] + #[sshwire(unknown)] + Other(Unknown<'a>), } #[derive(Debug, SSHEncode, SSHDecode)] From e41d1be8c343dbb89044daeb1c15bcd8bd9d82ee Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 25 Aug 2025 13:07:38 +1000 Subject: [PATCH 230/393] As per the pull request fails because of the edition, I am downgrading it 2024->2021 --- sftp/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 43385160..acd15ea5 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sunset-sftp" version = "0.1.0" -edition = "2024" +edition = "2021" [dependencies] sunset = { version = "0.3.0", path = "../" } From da9b5ccfd3286757f16d08c8523712a622419ed7 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 25 Aug 2025 13:52:45 +1000 Subject: [PATCH 231/393] Clarifying that this implementation will use SFTP version 3 As it is the most common version in use today, ensuring broad compatibility. Support for other versions may be considered in the future. --- sftp/src/proto.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index e44740e0..a0af6721 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -15,6 +15,8 @@ pub struct Filename<'a>(TextString<'a>); #[derive(Debug, SSHEncode, SSHDecode)] struct FileHandle<'a>(pub BinString<'a>); +/// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 +const SFTP_VERSION: u32 = 3; #[derive(Debug, SSHEncode, SSHDecode)] pub struct InitVersion { // No ReqId for SSH_FXP_INIT From 1af3776e4b50ff63444d36b89520007048c2f41c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 25 Aug 2025 15:58:16 +1000 Subject: [PATCH 232/393] Uncommenting macro_rules sftpmessages partially until problems arose with `Name` --- sftp/src/proto.rs | 145 +++++++++++++++++++++++----------------------- 1 file changed, 73 insertions(+), 72 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index a0af6721..8eda3a39 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -250,12 +250,12 @@ impl<'de> SSHDecode<'de> for Attrs { } } -// #[derive(Debug)] -// pub enum Error { -// UnknownPacket { number: u8 }, -// } +#[derive(Debug)] +pub enum Error { + UnknownPacket { number: u8 }, +} -// pub type Result = core::result::Result; +pub type Result = core::result::Result; // impl From for SunsetError { // fn from(error: Error) -> SunsetError { @@ -269,54 +269,62 @@ impl<'de> SSHDecode<'de> for Attrs { // } // } -// macro_rules! sftpmessages { -// ( -// $( ( $message_num:literal, -// $SpecificPacketVariant:ident, -// $SpecificPacketType:ty, -// $SSH_FXP_NAME:ident -// ), -// )* -// ) => { - -// #[derive(Debug, Clone)] -// #[repr(u8)] -// #[allow(non_camel_case_types)] -// pub enum SftpNum { -// // variants are eg -// // SSH_FXP_OPEN = 3, -// $( -// $SSH_FXP_NAME = $message_num, -// )* -// } +macro_rules! sftpmessages { + ( + $( ( $message_num:literal, + $SpecificPacketVariant:ident, + $SpecificPacketType:ty, + $SSH_FXP_NAME:ident + ), + )* + ) => { + #[derive(Debug, Clone)] + #[repr(u8)] + #[allow(non_camel_case_types)] + pub enum SftpNum { + // variants are eg + // SSH_FXP_OPEN = 3, + $( + $SSH_FXP_NAME = $message_num, + )* + } -// impl SftpNum { -// fn is_request(&self) -> bool { -// // TODO SSH_FXP_EXTENDED -// (2..=99).contains(&(self.clone() as u8)) -// } + impl SftpNum { + fn is_request(&self) -> bool { + // TODO SSH_FXP_EXTENDED + (2..=99).contains(&(self.clone() as u8)) + } -// fn is_response(&self) -> bool { -// // TODO SSH_FXP_EXTENDED_REPLY -// (100..=199).contains(&(self.clone() as u8)) -// } -// } + fn is_response(&self) -> bool { + // TODO SSH_FXP_EXTENDED_REPLY + (100..=199).contains(&(self.clone() as u8)) + } + } -// impl TryFrom for SftpNum { -// type Error = Error; -// fn try_from(v: u8) -> Result { -// match v { -// // eg -// // 3 => Ok(SftpNum::SSH_FXP_OPEN) -// $( -// $message_num => Ok(SftpNum::$SSH_FXP_NAME), -// )* -// _ => { -// Err(Error::UnknownPacket { number: v }) -// } -// } -// } -// } + impl TryFrom for SftpNum { + type Error = Error; + fn try_from(v: u8) -> Result { + match v { + // eg + // 3 => Ok(SftpNum::SSH_FXP_OPEN) + $( + $message_num => Ok(SftpNum::$SSH_FXP_NAME), + )* + _ => { + Err(Error::UnknownPacket { number: v }) + } + } + } + } + + // /// Top level SSH packet enum + // #[derive(Debug)] + // pub enum SftpPacket<'a> { + // // eg Open(Open<'a>), + // $( + // $SpecificPacketVariant($SpecificPacketType), + // )* + // } // impl SSHEncode for SftpPacket<'_> { // fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { @@ -359,14 +367,7 @@ impl<'de> SSHDecode<'de> for Attrs { // } // } -// /// Top level SSH packet enum -// #[derive(Debug)] -// pub enum SftpPacket<'a> { -// // eg Open(Open<'a>), -// $( -// $SpecificPacketVariant($SpecificPacketType), -// )* -// } + // impl<'a> SftpPacket<'a> { // pub fn sftp_num(&self) -> SftpNum { @@ -453,22 +454,22 @@ impl<'de> SSHDecode<'de> for Attrs { // } } // macro -// sftpmessages![ +sftpmessages![ -// // Message number ranges are also used by Sftpnum::is_request and is_response. +// Message number ranges are also used by Sftpnum::is_request and is_response. -// (1, Init, InitVersion, SSH_FXP_INIT), -// (2, Version, InitVersion, SSH_FXP_VERSION), +(1, Init, InitVersion, SSH_FXP_INIT), +(2, Version, InitVersion, SSH_FXP_VERSION), -// // Requests -// (3, Open, Open<'a>, SSH_FXP_OPEN), -// (4, Close, Close<'a>, SSH_FXP_CLOSE), -// (5, Read, Read<'a>, SSH_FXP_READ), +// Requests +(3, Open, Open<'a>, SSH_FXP_OPEN), +(4, Close, Close<'a>, SSH_FXP_CLOSE), +(5, Read, Read<'a>, SSH_FXP_READ), -// // Responses -// (101, Status, Status<'a>, SSH_FXP_STATUS), -// (102, Handle, Handle<'a>, SSH_FXP_HANDLE), -// (103, Data, Data<'a>, SSH_FXP_DATA), -// (104, Name, Name<'a>, SSH_FXP_NAME), +// Responses +(101, Status, Status<'a>, SSH_FXP_STATUS), +(102, Handle, Handle<'a>, SSH_FXP_HANDLE), +(103, Data, Data<'a>, SSH_FXP_DATA), +(104, Name, Name<'a>, SSH_FXP_NAME), -// ]; +]; From c1b63ae14668402e90560cfbfb1cabdb90655317 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 25 Aug 2025 15:59:58 +1000 Subject: [PATCH 233/393] Implementing SSHEncode and SSHDecode for Name Name has changed. It no longer contains count, but it Encodes it as the Names length --- sftp/src/proto.rs | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 8eda3a39..a67cc142 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -71,12 +71,6 @@ pub struct Data<'a> { pub data: BinString<'a>, } -#[derive(Debug, SSHEncode, SSHDecode)] -pub struct Name { - pub count: u32, - // TODO repeat NameEntry -} - #[derive(Debug, SSHEncode, SSHDecode)] pub struct NameEntry<'a> { pub filename: Filename<'a>, @@ -86,6 +80,37 @@ pub struct NameEntry<'a> { pub attrs: Attrs, } +#[derive(Debug)] +pub struct Name<'de>(pub Vec>); + +impl<'de> SSHDecode<'de> for Name<'de> { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + let count = u32::dec(s)? as usize; + + let mut names = Vec::with_capacity(count); + + for _ in 0..count { + names.push(NameEntry::dec(s)?); + } + + Ok(Name(names)) + } +} + +impl SSHEncode for Name<'_> { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + (self.0.len() as u32).enc(s)?; + + for element in self.0.iter() { + element.enc(s)?; + } + Ok(()) + } +} + #[derive(Debug, SSHEncode, SSHDecode)] pub struct ResponseAttributes { pub attrs: Attrs, From 0e6bba529f07e603b680f1b66c6d0299b9d8f07b Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 25 Aug 2025 16:02:31 +1000 Subject: [PATCH 234/393] adding macro_rules! sftpmessages closing curly brackets --- sftp/src/proto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index a67cc142..6aaf79e3 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -477,7 +477,7 @@ macro_rules! sftpmessages { // } // )* -// } } // macro +} } // macro sftpmessages![ From 85023fe27d0e9f3088102620f80edec3a2df4c6c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 25 Aug 2025 17:03:08 +1000 Subject: [PATCH 235/393] StatusCode: SSHEncode and SSHDecode Adding num_enum to sftp to handle the u32 to enum expansion --- Cargo.lock | 50 +++++++++++++++++++++++++++++++++++++++++------ sftp/Cargo.toml | 1 + sftp/src/proto.rs | 32 +++++++++++++++++++----------- 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80509a49..524ad539 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1812,11 +1812,12 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ - "num_enum_derive 0.7.3", + "num_enum_derive 0.7.4", + "rustversion", ] [[package]] @@ -1832,10 +1833,11 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.58", @@ -2011,7 +2013,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61d90fddc3d67f21bbf93683bc461b05d6a29c708caf3ffb79947d7ff7095406" dependencies = [ "arrayvec", - "num_enum 0.7.3", + "num_enum 0.7.4", "paste", ] @@ -2138,6 +2140,15 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -2880,6 +2891,7 @@ dependencies = [ name = "sunset-sftp" version = "0.1.0" dependencies = [ + "num_enum 0.7.4", "sunset", "sunset-sshwire-derive", ] @@ -3024,6 +3036,23 @@ dependencies = [ "syn 2.0.58", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "typenum" version = "1.17.0" @@ -3435,6 +3464,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + [[package]] name = "x25519-dalek" version = "2.0.1" diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index acd15ea5..7116ed01 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -4,5 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] +num_enum = {version = "0.7.4"} sunset = { version = "0.3.0", path = "../" } sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 6aaf79e3..8a6cada4 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,3 +1,4 @@ +use num_enum::{FromPrimitive, TryFromPrimitive}; use sunset::error; use sunset::error::Error as SunsetError; use sunset::packets::{MessageNumber, Packet, Unknown}; @@ -52,12 +53,12 @@ pub struct Write<'a> { // Responses -// #[derive(Debug, SSHEncode, SSHDecode)] -// pub struct Status<'a> { -// pub code: StatusCode, -// pub message: TextString<'a>, -// pub lang: TextString<'a>, -// } +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct Status<'a> { + pub code: StatusCode, + pub message: TextString<'a>, + pub lang: TextString<'a>, +} #[derive(Debug, SSHEncode, SSHDecode)] pub struct Handle<'a> { @@ -121,10 +122,10 @@ pub struct ResponseAttributes { #[derive(Debug, SSHEncode, SSHDecode, Clone, Copy)] pub struct ReqId(pub u32); -#[derive(Debug, SSHEncode, SSHDecode)] -#[repr(u8)] +#[derive(Debug, FromPrimitive, SSHEncode)] +#[repr(u32)] #[allow(non_camel_case_types)] -pub enum StatusCode<'a> { +pub enum StatusCode { #[sshwire(variant = "ssh_fx_ok")] SSH_FX_OK = 0, #[sshwire(variant = "ssh_fx_eof")] @@ -143,9 +144,18 @@ pub enum StatusCode<'a> { SSH_FX_CONNECTION_LOST = 7, #[sshwire(variant = "ssh_fx_unsupported")] SSH_FX_OP_UNSUPPORTED = 8, - // #[sshwire(variant = "ssh_fx_other")] #[sshwire(unknown)] - Other(Unknown<'a>), + #[num_enum(catch_all)] + Other(u32), +} + +impl<'de> SSHDecode<'de> for StatusCode { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + Ok(StatusCode::from(u32::dec(s)?)) + } } #[derive(Debug, SSHEncode, SSHDecode)] From c3b83eed864e6e388ef706f2d9eda6d2c8e16d11 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 26 Aug 2025 16:00:29 +1000 Subject: [PATCH 236/393] moving sftp edition to 2024 --- sftp/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 7116ed01..36c27867 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sunset-sftp" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] num_enum = {version = "0.7.4"} From 2dd8c7a731bb422d0ac51685f14037474b54b5a0 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 27 Aug 2025 17:12:26 +1000 Subject: [PATCH 237/393] WIP: Starting to implement SftpPacket modifying SftpNum - modified macro pattern messsage_num from literal to tt (token tree) - lifetimes for SftpPacket are back - Added dependency to crate paste for pattern modification (to lower case but can be use for many other things) - I had to duplicate InitVersion to avoid repetitions of implementations in the macro expansion --- sftp/Cargo.toml | 1 + sftp/src/proto.rs | 163 ++++++++++++++++++++++++++-------------------- 2 files changed, 94 insertions(+), 70 deletions(-) diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 36c27867..b875c003 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -7,3 +7,4 @@ edition = "2024" num_enum = {version = "0.7.4"} sunset = { version = "0.3.0", path = "../" } sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } +paste = "1.0" \ No newline at end of file diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 8a6cada4..838e5398 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -18,13 +18,23 @@ struct FileHandle<'a>(pub BinString<'a>); /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 const SFTP_VERSION: u32 = 3; + +/// The SFTP version of the client #[derive(Debug, SSHEncode, SSHDecode)] -pub struct InitVersion { +pub struct InitVersionClient { // No ReqId for SSH_FXP_INIT pub version: u32, // TODO variable number of ExtPair } +/// The lowers SFTP version from the client and the server +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct InitVersionLowest { + // No ReqId for SSH_FXP_VERSION + pub version: u32, + // TODO variable number of ExtPair +} + #[derive(Debug, SSHEncode, SSHDecode)] pub struct Open<'a> { pub filename: Filename<'a>, @@ -306,61 +316,72 @@ pub type Result = core::result::Result; macro_rules! sftpmessages { ( - $( ( $message_num:literal, + $( ( $message_num:tt, $SpecificPacketVariant:ident, $SpecificPacketType:ty, $SSH_FXP_NAME:ident ), )* ) => { - #[derive(Debug, Clone)] - #[repr(u8)] - #[allow(non_camel_case_types)] - pub enum SftpNum { - // variants are eg - // SSH_FXP_OPEN = 3, - $( - $SSH_FXP_NAME = $message_num, - )* + paste::paste! { + #[derive(Debug, Clone, FromPrimitive, SSHEncode)] + #[repr(u8)] + #[allow(non_camel_case_types)] + pub enum SftpNum { + // SSH_FXP_OPEN = 3, + $( + #[sshwire(variant = $SSH_FXP_NAME:lower)] + $SSH_FXP_NAME = $message_num, + )* + #[sshwire(unknown)] + #[num_enum(catch_all)] + Other(u8), + } + } // paste + + impl<'de> SSHDecode<'de> for SftpNum { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + Ok(SftpNum::from(u8::dec(s)?)) + } + } + + impl From for u8{ + fn from(sftp_num: SftpNum) -> u8 { + match sftp_num { + $( + SftpNum::$SSH_FXP_NAME => $message_num, // TODO: Fix this + )* + _ => 0 // Other, not in the enum definition + + } + } + } impl SftpNum { fn is_request(&self) -> bool { // TODO SSH_FXP_EXTENDED - (2..=99).contains(&(self.clone() as u8)) + (2..=99).contains(&(u8::from(self.clone()))) } fn is_response(&self) -> bool { // TODO SSH_FXP_EXTENDED_REPLY - (100..=199).contains(&(self.clone() as u8)) + (100..=199).contains(&(u8::from(self.clone()))) } } - impl TryFrom for SftpNum { - type Error = Error; - fn try_from(v: u8) -> Result { - match v { - // eg - // 3 => Ok(SftpNum::SSH_FXP_OPEN) - $( - $message_num => Ok(SftpNum::$SSH_FXP_NAME), - )* - _ => { - Err(Error::UnknownPacket { number: v }) - } - } - } + /// Top level SSH packet enum + #[derive(Debug)] + pub enum SftpPacket<'a> { + // eg Open(Open<'a>), + $( + $SpecificPacketVariant($SpecificPacketType), + )* } - // /// Top level SSH packet enum - // #[derive(Debug)] - // pub enum SftpPacket<'a> { - // // eg Open(Open<'a>), - // $( - // $SpecificPacketVariant($SpecificPacketType), - // )* - // } - // impl SSHEncode for SftpPacket<'_> { // fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { // let t = self.message_num() as u8; @@ -402,21 +423,21 @@ macro_rules! sftpmessages { // } // } + impl<'a> SftpPacket<'a> { + /// Maps `SpecificPacketVariant` to `message_num` + pub fn sftp_num(&self) -> SftpNum { + match self { + // eg + // SftpPacket::Open(_) => { + // .. + $( + SftpPacket::$SpecificPacketVariant(_) => { - -// impl<'a> SftpPacket<'a> { -// pub fn sftp_num(&self) -> SftpNum { -// match self { -// // eg -// // SftpPacket::Open(_) => { -// // .. -// $( -// SftpPacket::$SpecificPacketVariant(_) => { -// MessageNumber::$SSH_FXP_NAME -// } -// )* -// } -// } + SftpNum::from($message_num as u8) + } + )* + } + } // /// Encode a request. // /// @@ -479,13 +500,15 @@ macro_rules! sftpmessages { // } // } -// $( -// impl<'a> From<$SpecificPacketType> for SftpPacket<'a> { -// fn from(s: $SpecificPacketType) -> SftpPacket<'a> { -// SftpPacket::$SpecificPacketVariant(s) -// } -// } -// )* + } + +$( +impl<'a> From<$SpecificPacketType> for SftpPacket<'a> { + fn from(s: $SpecificPacketType) -> SftpPacket<'a> { + SftpPacket::$SpecificPacketVariant(s) //find me + } +} +)* } } // macro @@ -493,18 +516,18 @@ sftpmessages![ // Message number ranges are also used by Sftpnum::is_request and is_response. -(1, Init, InitVersion, SSH_FXP_INIT), -(2, Version, InitVersion, SSH_FXP_VERSION), - -// Requests -(3, Open, Open<'a>, SSH_FXP_OPEN), -(4, Close, Close<'a>, SSH_FXP_CLOSE), -(5, Read, Read<'a>, SSH_FXP_READ), - -// Responses -(101, Status, Status<'a>, SSH_FXP_STATUS), -(102, Handle, Handle<'a>, SSH_FXP_HANDLE), -(103, Data, Data<'a>, SSH_FXP_DATA), -(104, Name, Name<'a>, SSH_FXP_NAME), +(1, Init, InitVersionClient, SSH_FXP_INIT), + (2, Version, InitVersionLowest, SSH_FXP_VERSION), + // Requests + (3, Open, Open<'a>, SSH_FXP_OPEN), + (4, Close, Close<'a>, SSH_FXP_CLOSE), + (5, Read, Read<'a>, SSH_FXP_READ), + (6, Write, Write<'a>, SSH_FXP_WRITE), + + // Responses + (101, Status, Status<'a>, SSH_FXP_STATUS), + (102, Handle, Handle<'a>, SSH_FXP_HANDLE), + (103, Data, Data<'a>, SSH_FXP_DATA), + (104, Name, Name<'a>, SSH_FXP_NAME), ]; From a3133b9cbe385b4ec26aeff5576e050ffdaf8ccd Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 27 Aug 2025 18:23:57 +1000 Subject: [PATCH 238/393] Adding SSHEncode and SSHDecode for SftpPacket Updating cargo-expand-sftp.rs too --- sftp/out/cargo-expand-sftp.rs | 710 ++++++++++++++++++++++++++++++---- sftp/src/proto.rs | 80 ++-- 2 files changed, 681 insertions(+), 109 deletions(-) diff --git a/sftp/out/cargo-expand-sftp.rs b/sftp/out/cargo-expand-sftp.rs index b2766361..bbdd7f37 100644 --- a/sftp/out/cargo-expand-sftp.rs +++ b/sftp/out/cargo-expand-sftp.rs @@ -1,9 +1,10 @@ #![feature(prelude_import)] -#[macro_use] -extern crate std; #[prelude_import] use std::prelude::rust_2024::*; +#[macro_use] +extern crate std; mod proto { + use num_enum::FromPrimitive; use sunset::error; use sunset::error::Error as SunsetError; use sunset::packets::{MessageNumber, Packet, Unknown}; @@ -66,22 +67,25 @@ mod proto { Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) } } - pub struct InitVersion { + /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 + const SFTP_VERSION: u32 = 3; + /// The SFTP version of the client + pub struct InitVersionClient { pub version: u32, } #[automatically_derived] - impl ::core::fmt::Debug for InitVersion { + impl ::core::fmt::Debug for InitVersionClient { #[inline] fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { ::core::fmt::Formatter::debug_struct_field1_finish( f, - "InitVersion", + "InitVersionClient", "version", &&self.version, ) } } - impl ::sunset::sshwire::SSHEncode for InitVersion { + impl ::sunset::sshwire::SSHEncode for InitVersionClient { fn enc( &self, s: &mut dyn ::sunset::sshwire::SSHSink, @@ -90,7 +94,40 @@ mod proto { Ok(()) } } - impl<'de> ::sunset::sshwire::SSHDecode<'de> for InitVersion { + impl<'de> ::sunset::sshwire::SSHDecode<'de> for InitVersionClient { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_version = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { version: field_version }) + } + } + /// The lowers SFTP version from the client and the server + pub struct InitVersionLowest { + pub version: u32, + } + #[automatically_derived] + impl ::core::fmt::Debug for InitVersionLowest { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "InitVersionLowest", + "version", + &&self.version, + ) + } + } + impl ::sunset::sshwire::SSHEncode for InitVersionLowest { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.version, s)?; + Ok(()) + } + } + impl<'de> ::sunset::sshwire::SSHDecode<'de> for InitVersionLowest { fn dec>( s: &mut S, ) -> ::sunset::sshwire::WireResult { @@ -280,6 +317,55 @@ mod proto { }) } } + pub struct Status<'a> { + pub code: StatusCode, + pub message: TextString<'a>, + pub lang: TextString<'a>, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for Status<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field3_finish( + f, + "Status", + "code", + &self.code, + "message", + &self.message, + "lang", + &&self.lang, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for Status<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.code, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.message, s)?; + ::sunset::sshwire::SSHEncode::enc(&self.lang, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Status<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_code = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_message = ::sunset::sshwire::SSHDecode::dec(s)?; + let field_lang = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { + code: field_code, + message: field_message, + lang: field_lang, + }) + } + } pub struct Handle<'a> { pub handle: FileHandle<'a>, } @@ -364,38 +450,6 @@ mod proto { }) } } - pub struct Name { - pub count: u32, - } - #[automatically_derived] - impl ::core::fmt::Debug for Name { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "Name", - "count", - &&self.count, - ) - } - } - impl ::sunset::sshwire::SSHEncode for Name { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.count, s)?; - Ok(()) - } - } - impl<'de> ::sunset::sshwire::SSHDecode<'de> for Name { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_count = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { count: field_count }) - } - } pub struct NameEntry<'a> { pub filename: Filename<'a>, /// longname is an undefined text line like "ls -l", @@ -447,6 +501,36 @@ mod proto { }) } } + pub struct Name<'de>(pub Vec>); + #[automatically_derived] + impl<'de> ::core::fmt::Debug for Name<'de> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Name", &&self.0) + } + } + impl<'de> SSHDecode<'de> for Name<'de> { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + let count = u32::dec(s)? as usize; + let mut names = Vec::with_capacity(count); + for _ in 0..count { + names.push(NameEntry::dec(s)?); + } + Ok(Name(names)) + } + } + impl SSHEncode for Name<'_> { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + (self.0.len() as u32).enc(s)?; + for element in self.0.iter() { + element.enc(s)?; + } + Ok(()) + } + } pub struct ResponseAttributes { pub attrs: Attrs, } @@ -513,9 +597,9 @@ mod proto { } #[automatically_derived] impl ::core::marker::Copy for ReqId {} - #[repr(u8)] + #[repr(u32)] #[allow(non_camel_case_types)] - pub enum StatusCode<'a> { + pub enum StatusCode { #[sshwire(variant = "ssh_fx_ok")] SSH_FX_OK = 0, #[sshwire(variant = "ssh_fx_eof")] @@ -535,11 +619,12 @@ mod proto { #[sshwire(variant = "ssh_fx_unsupported")] SSH_FX_OP_UNSUPPORTED = 8, #[sshwire(unknown)] - Other(Unknown<'a>), + #[num_enum(catch_all)] + Other(u32), } #[automatically_derived] #[allow(non_camel_case_types)] - impl<'a> ::core::fmt::Debug for StatusCode<'a> { + impl ::core::fmt::Debug for StatusCode { #[inline] fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { match self { @@ -580,7 +665,44 @@ mod proto { } } } - impl<'a> ::sunset::sshwire::SSHEncode for StatusCode<'a> { + impl ::num_enum::FromPrimitive for StatusCode { + type Primitive = u32; + fn from_primitive(number: Self::Primitive) -> Self { + #![allow(non_upper_case_globals)] + const SSH_FX_OK__num_enum_0__: u32 = 0; + const SSH_FX_EOF__num_enum_0__: u32 = 1; + const SSH_FX_NO_SUCH_FILE__num_enum_0__: u32 = 2; + const SSH_FX_PERMISSION_DENIED__num_enum_0__: u32 = 3; + const SSH_FX_FAILURE__num_enum_0__: u32 = 4; + const SSH_FX_BAD_MESSAGE__num_enum_0__: u32 = 5; + const SSH_FX_NO_CONNECTION__num_enum_0__: u32 = 6; + const SSH_FX_CONNECTION_LOST__num_enum_0__: u32 = 7; + const SSH_FX_OP_UNSUPPORTED__num_enum_0__: u32 = 8; + #[deny(unreachable_patterns)] + match number { + SSH_FX_OK__num_enum_0__ => Self::SSH_FX_OK, + SSH_FX_EOF__num_enum_0__ => Self::SSH_FX_EOF, + SSH_FX_NO_SUCH_FILE__num_enum_0__ => Self::SSH_FX_NO_SUCH_FILE, + SSH_FX_PERMISSION_DENIED__num_enum_0__ => Self::SSH_FX_PERMISSION_DENIED, + SSH_FX_FAILURE__num_enum_0__ => Self::SSH_FX_FAILURE, + SSH_FX_BAD_MESSAGE__num_enum_0__ => Self::SSH_FX_BAD_MESSAGE, + SSH_FX_NO_CONNECTION__num_enum_0__ => Self::SSH_FX_NO_CONNECTION, + SSH_FX_CONNECTION_LOST__num_enum_0__ => Self::SSH_FX_CONNECTION_LOST, + SSH_FX_OP_UNSUPPORTED__num_enum_0__ => Self::SSH_FX_OP_UNSUPPORTED, + #[allow(unreachable_patterns)] + _ => Self::Other(number), + } + } + } + impl ::core::convert::From for StatusCode { + #[inline] + fn from(number: u32) -> Self { + ::num_enum::FromPrimitive::from_primitive(number) + } + } + #[doc(hidden)] + impl ::num_enum::CannotDeriveBothFromPrimitiveAndTryFromPrimitive for StatusCode {} + impl ::sunset::sshwire::SSHEncode for StatusCode { fn enc( &self, s: &mut dyn ::sunset::sshwire::SSHSink, @@ -602,7 +724,7 @@ mod proto { #[allow(unreachable_code)] Ok(()) } } - impl<'a> ::sunset::sshwire::SSHEncodeEnum for StatusCode<'a> { + impl ::sunset::sshwire::SSHEncodeEnum for StatusCode { fn variant_name(&self) -> ::sunset::sshwire::WireResult<&'static str> { let r = match self { Self::SSH_FX_OK => "ssh_fx_ok", @@ -621,31 +743,12 @@ mod proto { #[allow(unreachable_code)] Ok(r) } } - impl<'de, 'a> ::sunset::sshwire::SSHDecodeEnum<'de> for StatusCode<'a> - where - 'de: 'a, - { - fn dec_enum>( - s: &mut S, - variant: &'de [u8], - ) -> ::sunset::sshwire::WireResult { - let var_str = ::sunset::sshwire::try_as_ascii_str(variant).ok(); - let r = match var_str { - Some("ssh_fx_ok") => Self::SSH_FX_OK, - Some("ssh_fx_eof") => Self::SSH_FX_EOF, - Some("ssh_fx_no_such_file") => Self::SSH_FX_NO_SUCH_FILE, - Some("ssh_fx_permission_denied") => Self::SSH_FX_PERMISSION_DENIED, - Some("ssh_fx_failure") => Self::SSH_FX_FAILURE, - Some("ssh_fx_bad_message") => Self::SSH_FX_BAD_MESSAGE, - Some("ssh_fx_no_connection") => Self::SSH_FX_NO_CONNECTION, - Some("ssh_fx_connection_lost") => Self::SSH_FX_CONNECTION_LOST, - Some("ssh_fx_unsupported") => Self::SSH_FX_OP_UNSUPPORTED, - _ => { - s.ctx().seen_unknown = true; - Self::Other(Unknown::new(variant)) - } - }; - Ok(r) + impl<'de> SSHDecode<'de> for StatusCode { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + Ok(StatusCode::from(u32::dec(s)?)) } } pub struct ExtPair<'a> { @@ -826,4 +929,471 @@ mod proto { Ok(attrs) } } + pub enum Error { + UnknownPacket { number: u8 }, + } + #[automatically_derived] + impl ::core::fmt::Debug for Error { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + Error::UnknownPacket { number: __self_0 } => { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "UnknownPacket", + "number", + &__self_0, + ) + } + } + } + } + pub type Result = core::result::Result; + #[repr(u8)] + #[allow(non_camel_case_types)] + pub enum SftpNum { + #[sshwire(variant = "ssh_fxp_init")] + SSH_FXP_INIT = 1, + #[sshwire(variant = "ssh_fxp_version")] + SSH_FXP_VERSION = 2, + #[sshwire(variant = "ssh_fxp_open")] + SSH_FXP_OPEN = 3, + #[sshwire(variant = "ssh_fxp_close")] + SSH_FXP_CLOSE = 4, + #[sshwire(variant = "ssh_fxp_read")] + SSH_FXP_READ = 5, + #[sshwire(variant = "ssh_fxp_write")] + SSH_FXP_WRITE = 6, + #[sshwire(variant = "ssh_fxp_status")] + SSH_FXP_STATUS = 101, + #[sshwire(variant = "ssh_fxp_handle")] + SSH_FXP_HANDLE = 102, + #[sshwire(variant = "ssh_fxp_data")] + SSH_FXP_DATA = 103, + #[sshwire(variant = "ssh_fxp_name")] + SSH_FXP_NAME = 104, + #[sshwire(unknown)] + #[num_enum(catch_all)] + Other(u8), + } + #[automatically_derived] + #[allow(non_camel_case_types)] + impl ::core::fmt::Debug for SftpNum { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + SftpNum::SSH_FXP_INIT => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_INIT") + } + SftpNum::SSH_FXP_VERSION => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_VERSION") + } + SftpNum::SSH_FXP_OPEN => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_OPEN") + } + SftpNum::SSH_FXP_CLOSE => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_CLOSE") + } + SftpNum::SSH_FXP_READ => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_READ") + } + SftpNum::SSH_FXP_WRITE => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_WRITE") + } + SftpNum::SSH_FXP_STATUS => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_STATUS") + } + SftpNum::SSH_FXP_HANDLE => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_HANDLE") + } + SftpNum::SSH_FXP_DATA => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_DATA") + } + SftpNum::SSH_FXP_NAME => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_NAME") + } + SftpNum::Other(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Other", + &__self_0, + ) + } + } + } + } + #[automatically_derived] + #[allow(non_camel_case_types)] + impl ::core::clone::Clone for SftpNum { + #[inline] + fn clone(&self) -> SftpNum { + match self { + SftpNum::SSH_FXP_INIT => SftpNum::SSH_FXP_INIT, + SftpNum::SSH_FXP_VERSION => SftpNum::SSH_FXP_VERSION, + SftpNum::SSH_FXP_OPEN => SftpNum::SSH_FXP_OPEN, + SftpNum::SSH_FXP_CLOSE => SftpNum::SSH_FXP_CLOSE, + SftpNum::SSH_FXP_READ => SftpNum::SSH_FXP_READ, + SftpNum::SSH_FXP_WRITE => SftpNum::SSH_FXP_WRITE, + SftpNum::SSH_FXP_STATUS => SftpNum::SSH_FXP_STATUS, + SftpNum::SSH_FXP_HANDLE => SftpNum::SSH_FXP_HANDLE, + SftpNum::SSH_FXP_DATA => SftpNum::SSH_FXP_DATA, + SftpNum::SSH_FXP_NAME => SftpNum::SSH_FXP_NAME, + SftpNum::Other(__self_0) => { + SftpNum::Other(::core::clone::Clone::clone(__self_0)) + } + } + } + } + impl ::num_enum::FromPrimitive for SftpNum { + type Primitive = u8; + fn from_primitive(number: Self::Primitive) -> Self { + #![allow(non_upper_case_globals)] + const SSH_FXP_INIT__num_enum_0__: u8 = 1; + const SSH_FXP_VERSION__num_enum_0__: u8 = 2; + const SSH_FXP_OPEN__num_enum_0__: u8 = 3; + const SSH_FXP_CLOSE__num_enum_0__: u8 = 4; + const SSH_FXP_READ__num_enum_0__: u8 = 5; + const SSH_FXP_WRITE__num_enum_0__: u8 = 6; + const SSH_FXP_STATUS__num_enum_0__: u8 = 101; + const SSH_FXP_HANDLE__num_enum_0__: u8 = 102; + const SSH_FXP_DATA__num_enum_0__: u8 = 103; + const SSH_FXP_NAME__num_enum_0__: u8 = 104; + #[deny(unreachable_patterns)] + match number { + SSH_FXP_INIT__num_enum_0__ => Self::SSH_FXP_INIT, + SSH_FXP_VERSION__num_enum_0__ => Self::SSH_FXP_VERSION, + SSH_FXP_OPEN__num_enum_0__ => Self::SSH_FXP_OPEN, + SSH_FXP_CLOSE__num_enum_0__ => Self::SSH_FXP_CLOSE, + SSH_FXP_READ__num_enum_0__ => Self::SSH_FXP_READ, + SSH_FXP_WRITE__num_enum_0__ => Self::SSH_FXP_WRITE, + SSH_FXP_STATUS__num_enum_0__ => Self::SSH_FXP_STATUS, + SSH_FXP_HANDLE__num_enum_0__ => Self::SSH_FXP_HANDLE, + SSH_FXP_DATA__num_enum_0__ => Self::SSH_FXP_DATA, + SSH_FXP_NAME__num_enum_0__ => Self::SSH_FXP_NAME, + #[allow(unreachable_patterns)] + _ => Self::Other(number), + } + } + } + impl ::core::convert::From for SftpNum { + #[inline] + fn from(number: u8) -> Self { + ::num_enum::FromPrimitive::from_primitive(number) + } + } + #[doc(hidden)] + impl ::num_enum::CannotDeriveBothFromPrimitiveAndTryFromPrimitive for SftpNum {} + impl ::sunset::sshwire::SSHEncode for SftpNum { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + match *self { + Self::SSH_FXP_INIT => {} + Self::SSH_FXP_VERSION => {} + Self::SSH_FXP_OPEN => {} + Self::SSH_FXP_CLOSE => {} + Self::SSH_FXP_READ => {} + Self::SSH_FXP_WRITE => {} + Self::SSH_FXP_STATUS => {} + Self::SSH_FXP_HANDLE => {} + Self::SSH_FXP_DATA => {} + Self::SSH_FXP_NAME => {} + Self::Other(ref i) => { + return Err(::sunset::sshwire::WireError::UnknownVariant); + } + } + #[allow(unreachable_code)] Ok(()) + } + } + impl ::sunset::sshwire::SSHEncodeEnum for SftpNum { + fn variant_name(&self) -> ::sunset::sshwire::WireResult<&'static str> { + let r = match self { + Self::SSH_FXP_INIT => "ssh_fxp_init", + Self::SSH_FXP_VERSION => "ssh_fxp_version", + Self::SSH_FXP_OPEN => "ssh_fxp_open", + Self::SSH_FXP_CLOSE => "ssh_fxp_close", + Self::SSH_FXP_READ => "ssh_fxp_read", + Self::SSH_FXP_WRITE => "ssh_fxp_write", + Self::SSH_FXP_STATUS => "ssh_fxp_status", + Self::SSH_FXP_HANDLE => "ssh_fxp_handle", + Self::SSH_FXP_DATA => "ssh_fxp_data", + Self::SSH_FXP_NAME => "ssh_fxp_name", + Self::Other(_) => { + return Err(::sunset::sshwire::WireError::UnknownVariant); + } + }; + #[allow(unreachable_code)] Ok(r) + } + } + impl<'de> SSHDecode<'de> for SftpNum { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + Ok(SftpNum::from(u8::dec(s)?)) + } + } + impl From for u8 { + fn from(sftp_num: SftpNum) -> u8 { + match sftp_num { + SftpNum::SSH_FXP_INIT => 1, + SftpNum::SSH_FXP_VERSION => 2, + SftpNum::SSH_FXP_OPEN => 3, + SftpNum::SSH_FXP_CLOSE => 4, + SftpNum::SSH_FXP_READ => 5, + SftpNum::SSH_FXP_WRITE => 6, + SftpNum::SSH_FXP_STATUS => 101, + SftpNum::SSH_FXP_HANDLE => 102, + SftpNum::SSH_FXP_DATA => 103, + SftpNum::SSH_FXP_NAME => 104, + _ => 0, + } + } + } + impl SftpNum { + fn is_request(&self) -> bool { + (2..=99).contains(&(u8::from(self.clone()))) + } + fn is_response(&self) -> bool { + (100..=199).contains(&(u8::from(self.clone()))) + } + } + /// Top level SSH packet enum + /// + /// It helps identifying the SFTP Packet type and handling it accordingly + /// This is done using the SFTP field type + pub enum SftpPacket<'a> { + Init(InitVersionClient), + Version(InitVersionLowest), + Open(Open<'a>), + Close(Close<'a>), + Read(Read<'a>), + Write(Write<'a>), + Status(Status<'a>), + Handle(Handle<'a>), + Data(Data<'a>), + Name(Name<'a>), + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for SftpPacket<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + SftpPacket::Init(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Init", + &__self_0, + ) + } + SftpPacket::Version(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Version", + &__self_0, + ) + } + SftpPacket::Open(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Open", + &__self_0, + ) + } + SftpPacket::Close(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Close", + &__self_0, + ) + } + SftpPacket::Read(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Read", + &__self_0, + ) + } + SftpPacket::Write(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Write", + &__self_0, + ) + } + SftpPacket::Status(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Status", + &__self_0, + ) + } + SftpPacket::Handle(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Handle", + &__self_0, + ) + } + SftpPacket::Data(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Data", + &__self_0, + ) + } + SftpPacket::Name(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "Name", + &__self_0, + ) + } + } + } + } + impl SSHEncode for SftpPacket<'_> { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + let t = u8::from(self.sftp_num()); + t.enc(s)?; + match self { + SftpPacket::Init(p) => p.enc(s)?, + SftpPacket::Version(p) => p.enc(s)?, + SftpPacket::Open(p) => p.enc(s)?, + SftpPacket::Close(p) => p.enc(s)?, + SftpPacket::Read(p) => p.enc(s)?, + SftpPacket::Write(p) => p.enc(s)?, + SftpPacket::Status(p) => p.enc(s)?, + SftpPacket::Handle(p) => p.enc(s)?, + SftpPacket::Data(p) => p.enc(s)?, + SftpPacket::Name(p) => p.enc(s)?, + }; + Ok(()) + } + } + impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + let packet_type_number = u8::dec(s)?; + let packet_type = SftpNum::from(packet_type_number); + let decoded_packet = match packet_type { + SSH_FXP_INIT => { + let inner_type = ::dec(s)?; + SftpPacket::Init(inner_type) + } + SSH_FXP_VERSION => { + let inner_type = ::dec(s)?; + SftpPacket::Version(inner_type) + } + SSH_FXP_OPEN => { + let inner_type = >::dec(s)?; + SftpPacket::Open(inner_type) + } + SSH_FXP_CLOSE => { + let inner_type = >::dec(s)?; + SftpPacket::Close(inner_type) + } + SSH_FXP_READ => { + let inner_type = >::dec(s)?; + SftpPacket::Read(inner_type) + } + SSH_FXP_WRITE => { + let inner_type = >::dec(s)?; + SftpPacket::Write(inner_type) + } + SSH_FXP_STATUS => { + let inner_type = >::dec(s)?; + SftpPacket::Status(inner_type) + } + SSH_FXP_HANDLE => { + let inner_type = >::dec(s)?; + SftpPacket::Handle(inner_type) + } + SSH_FXP_DATA => { + let inner_type = >::dec(s)?; + SftpPacket::Data(inner_type) + } + SSH_FXP_NAME => { + let inner_type = >::dec(s)?; + SftpPacket::Name(inner_type) + } + _ => { + return Err(WireError::UnknownPacket { + number: packet_type_number, + }); + } + }; + Ok(decoded_packet) + } + } + impl<'a> SftpPacket<'a> { + /// Maps `SpecificPacketVariant` to `message_num` + pub fn sftp_num(&self) -> SftpNum { + match self { + SftpPacket::Init(_) => SftpNum::from(1 as u8), + SftpPacket::Version(_) => SftpNum::from(2 as u8), + SftpPacket::Open(_) => SftpNum::from(3 as u8), + SftpPacket::Close(_) => SftpNum::from(4 as u8), + SftpPacket::Read(_) => SftpNum::from(5 as u8), + SftpPacket::Write(_) => SftpNum::from(6 as u8), + SftpPacket::Status(_) => SftpNum::from(101 as u8), + SftpPacket::Handle(_) => SftpNum::from(102 as u8), + SftpPacket::Data(_) => SftpNum::from(103 as u8), + SftpPacket::Name(_) => SftpNum::from(104 as u8), + } + } + } + impl<'a> From for SftpPacket<'a> { + fn from(s: InitVersionClient) -> SftpPacket<'a> { + SftpPacket::Init(s) + } + } + impl<'a> From for SftpPacket<'a> { + fn from(s: InitVersionLowest) -> SftpPacket<'a> { + SftpPacket::Version(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Open<'a>) -> SftpPacket<'a> { + SftpPacket::Open(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Close<'a>) -> SftpPacket<'a> { + SftpPacket::Close(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Read<'a>) -> SftpPacket<'a> { + SftpPacket::Read(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Write<'a>) -> SftpPacket<'a> { + SftpPacket::Write(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Status<'a>) -> SftpPacket<'a> { + SftpPacket::Status(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Handle<'a>) -> SftpPacket<'a> { + SftpPacket::Handle(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Data<'a>) -> SftpPacket<'a> { + SftpPacket::Data(s) + } + } + impl<'a> From> for SftpPacket<'a> { + fn from(s: Name<'a>) -> SftpPacket<'a> { + SftpPacket::Name(s) + } + } } diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 838e5398..037f0a20 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -374,6 +374,9 @@ macro_rules! sftpmessages { } /// Top level SSH packet enum + /// + /// It helps identifying the SFTP Packet type and handling it accordingly + /// This is done using the SFTP field type #[derive(Debug)] pub enum SftpPacket<'a> { // eg Open(Open<'a>), @@ -382,46 +385,45 @@ macro_rules! sftpmessages { )* } -// impl SSHEncode for SftpPacket<'_> { -// fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { -// let t = self.message_num() as u8; -// t.enc(s)?; -// match self { -// // eg -// // Packet::KexInit(p) => { -// // ... -// $( -// Packet::$SpecificPacketVariant(p) => { -// p.enc(s)? -// } -// )* -// }; -// Ok(()) -// } -// } + impl SSHEncode for SftpPacket<'_> { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + let t = u8::from(self.sftp_num()); + t.enc(s)?; + match self { + // eg + // SftpPacket::KexInit(p) => { + // ... + $( + SftpPacket::$SpecificPacketVariant(p) => { + p.enc(s)? + } + )* + }; + Ok(()) + } + } -// impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { -// fn dec(s: &mut S) -> WireResult -// where S: SSHSource<'de> { -// let msg_num = u8::dec(s)?; -// let ty = MessageNumber::try_from(msg_num); -// let ty = match ty { -// Ok(t) => t, -// Err(_) => return Err(WireError::UnknownPacket { number: msg_num }) -// }; - -// // Decode based on the message number -// let p = match ty { -// // eg -// // MessageNumber::SSH_MSG_KEXINIT => Packet::KexInit( -// // ... -// $( -// MessageNumber::$SSH_FXP_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?), -// )* -// }; -// Ok(p) -// } -// } + impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> + where 'a: 'de, + { + fn dec(s: &mut S) -> WireResult + where S: SSHSource<'de> { + let packet_type_number = u8::dec(s)?; + + let packet_type = SftpNum::from(packet_type_number); + + let decoded_packet = match packet_type { + $( + SftpNum::$SSH_FXP_NAME => { + let inner_type = <$SpecificPacketType>::dec(s)?; + SftpPacket::$SpecificPacketVariant(inner_type) + }, + )* + _ => return Err(WireError::UnknownPacket { number: packet_type_number }) + }; + Ok(decoded_packet) + } + } impl<'a> SftpPacket<'a> { /// Maps `SpecificPacketVariant` to `message_num` From 371ca7711ae215d83a93832b8044f38cd8bec622 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 28 Aug 2025 13:44:56 +1000 Subject: [PATCH 239/393] Added past dependency in Cargo.lock and as use --- Cargo.lock | 1 + sftp/src/proto.rs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 524ad539..8dcaffa8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2892,6 +2892,7 @@ name = "sunset-sftp" version = "0.1.0" dependencies = [ "num_enum 0.7.4", + "paste", "sunset", "sunset-sshwire-derive", ] diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 037f0a20..0018e620 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,4 +1,5 @@ -use num_enum::{FromPrimitive, TryFromPrimitive}; +use num_enum::FromPrimitive; +use paste::paste; use sunset::error; use sunset::error::Error as SunsetError; use sunset::packets::{MessageNumber, Packet, Unknown}; @@ -323,7 +324,7 @@ macro_rules! sftpmessages { ), )* ) => { - paste::paste! { + paste! { #[derive(Debug, Clone, FromPrimitive, SSHEncode)] #[repr(u8)] #[allow(non_camel_case_types)] From 14a2ae6016e37cfbb812954f1980d88e1377b6d3 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 28 Aug 2025 13:47:29 +1000 Subject: [PATCH 240/393] Restricting SSHDecode for SftpPacket lifetimes so they match It is convoluted, changing unifying lifetimes for this implementation to `'a` would work as well --- sftp/out/cargo-expand-sftp.rs | 26 +++++++++++++++----------- sftp/src/proto.rs | 5 +++-- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/sftp/out/cargo-expand-sftp.rs b/sftp/out/cargo-expand-sftp.rs index bbdd7f37..996d330f 100644 --- a/sftp/out/cargo-expand-sftp.rs +++ b/sftp/out/cargo-expand-sftp.rs @@ -5,6 +5,7 @@ use std::prelude::rust_2024::*; extern crate std; mod proto { use num_enum::FromPrimitive; + use paste::paste; use sunset::error; use sunset::error::Error as SunsetError; use sunset::packets::{MessageNumber, Packet, Unknown}; @@ -1272,7 +1273,10 @@ mod proto { Ok(()) } } - impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> { + impl<'a: 'de, 'de> SSHDecode<'de> for SftpPacket<'a> + where + 'de: 'a, + { fn dec(s: &mut S) -> WireResult where S: SSHSource<'de>, @@ -1280,43 +1284,43 @@ mod proto { let packet_type_number = u8::dec(s)?; let packet_type = SftpNum::from(packet_type_number); let decoded_packet = match packet_type { - SSH_FXP_INIT => { + SftpNum::SSH_FXP_INIT => { let inner_type = ::dec(s)?; SftpPacket::Init(inner_type) } - SSH_FXP_VERSION => { + SftpNum::SSH_FXP_VERSION => { let inner_type = ::dec(s)?; SftpPacket::Version(inner_type) } - SSH_FXP_OPEN => { + SftpNum::SSH_FXP_OPEN => { let inner_type = >::dec(s)?; SftpPacket::Open(inner_type) } - SSH_FXP_CLOSE => { + SftpNum::SSH_FXP_CLOSE => { let inner_type = >::dec(s)?; SftpPacket::Close(inner_type) } - SSH_FXP_READ => { + SftpNum::SSH_FXP_READ => { let inner_type = >::dec(s)?; SftpPacket::Read(inner_type) } - SSH_FXP_WRITE => { + SftpNum::SSH_FXP_WRITE => { let inner_type = >::dec(s)?; SftpPacket::Write(inner_type) } - SSH_FXP_STATUS => { + SftpNum::SSH_FXP_STATUS => { let inner_type = >::dec(s)?; SftpPacket::Status(inner_type) } - SSH_FXP_HANDLE => { + SftpNum::SSH_FXP_HANDLE => { let inner_type = >::dec(s)?; SftpPacket::Handle(inner_type) } - SSH_FXP_DATA => { + SftpNum::SSH_FXP_DATA => { let inner_type = >::dec(s)?; SftpPacket::Data(inner_type) } - SSH_FXP_NAME => { + SftpNum::SSH_FXP_NAME => { let inner_type = >::dec(s)?; SftpPacket::Name(inner_type) } diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 0018e620..832fed75 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -404,8 +404,9 @@ macro_rules! sftpmessages { } } - impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> - where 'a: 'de, + + impl<'a: 'de, 'de> SSHDecode<'de> for SftpPacket<'a> + where 'de: 'a // This implies that both lifetimes are equal { fn dec(s: &mut S) -> WireResult where S: SSHSource<'de> { From c40280e40660e56ec1ba38f137de434eea492376 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 28 Aug 2025 14:39:18 +1000 Subject: [PATCH 241/393] cargo check passing and all the code uncommented. However... I have changed the signature for methods `decode_request` and `decode_response` to avoid the dyn SSHSource which was causing a problen during calls to dec(s)? Since it does not have a known size at compile time. To do so I have included the `'de` a lifetime for the method parameter s and made the structure SftpPacket lifetime `'a` and `'de` be equal, as I did in the previous commit fab799627f96f879df874ae016182937c3bd5f1e Will this be an issue? Last, the checks to find out if the decoded packet is a response or a request in `decode_response` and `decode_request` now return WireError::PacketWrong instead of error::SSHProto.fail() or Error::bug(). This might need to change --- sftp/out/cargo-expand-sftp.rs | 54 +++++++++++++++ sftp/src/proto.rs | 126 ++++++++++++++++++++-------------- 2 files changed, 128 insertions(+), 52 deletions(-) diff --git a/sftp/out/cargo-expand-sftp.rs b/sftp/out/cargo-expand-sftp.rs index 996d330f..0c712792 100644 --- a/sftp/out/cargo-expand-sftp.rs +++ b/sftp/out/cargo-expand-sftp.rs @@ -1349,6 +1349,60 @@ mod proto { SftpPacket::Name(_) => SftpNum::from(104 as u8), } } + /// Encode a request. + /// + /// Used by a SFTP client. Does not include the length field. + pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { + if !self.sftp_num().is_request() { + return Err(WireError::PacketWrong); + } + self.sftp_num().enc(s)?; + id.0.enc(s)?; + self.enc(s) + } + /// Decode a response. + /// + /// Used by a SFTP client. Does not include the length field. + pub fn decode_response<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> + where + S: SSHSource<'de>, + 'a: 'de, + 'de: 'a, + { + let num = SftpNum::from(u8::dec(s)?); + if !num.is_response() { + return Err(WireError::PacketWrong); + } + let id = ReqId(u32::dec(s)?); + Ok((id, Self::dec(s)?)) + } + /// Decode a request. + /// + /// Used by a SFTP server. Does not include the length field. + pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> + where + S: SSHSource<'de>, + 'a: 'de, + 'de: 'a, + { + let num = SftpNum::from(u8::dec(s)?); + if !num.is_request() { + return Err(WireError::PacketWrong); + } + let id = ReqId(u32::dec(s)?); + Ok((id, Self::dec(s)?)) + } + /// Encode a response. + /// + /// Used by a SFTP server. Does not include the length field. + pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { + if !self.sftp_num().is_response() { + return Err(WireError::PacketWrong); + } + self.sftp_num().enc(s)?; + id.0.enc(s)?; + self.enc(s) + } } impl<'a> From for SftpPacket<'a> { fn from(s: InitVersionClient) -> SftpPacket<'a> { diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 832fed75..5cc74343 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -443,66 +443,89 @@ macro_rules! sftpmessages { } } -// /// Encode a request. -// /// -// /// Used by a SFTP client. Does not include the length field. -// pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> { -// if !self.sftp_num().is_request() { -// return Err(Error::bug()) -// } + /// Encode a request. + /// + /// Used by a SFTP client. Does not include the length field. + pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { + // TODO: handle the Error conversion + if !self.sftp_num().is_request() { + return Err(WireError::PacketWrong) + // return Err(Error::bug()) + // TODO: I understand that it would be a bad call of encode_response and + // therefore a bug, bug Error::bug() is not compatible with WireResult + } -// // packet type -// self.sftp_num().enc(s)?; -// // request ID -// id.0.enc(s)?; -// // contents -// self.enc(s) -// } + // packet type + self.sftp_num().enc(s)?; + // request ID + id.0.enc(s)?; + // contents + self.enc(s) + } -// /// Decode a response. -// /// -// /// Used by a SFTP client. Does not include the length field. -// pub fn decode_response(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> { -// let num = SftpNum::try_from(u8::dec(s)?)?; + /// Decode a response. + /// + /// Used by a SFTP client. Does not include the length field. + pub fn decode_response<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> + where + S: SSHSource<'de>, + 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes + 'de: 'a + { + let num = SftpNum::from(u8::dec(s)?); -// if !num.is_response() { -// return error::SSHProto.fail(); -// } + if !num.is_response() { + return Err(WireError::PacketWrong) + // return error::SSHProto.fail(); + // TODO: Not an error in the SSHProtocol rather the SFTP. + // Maybe is time to define an SftpError + } -// let id = ReqId(u32::dec(s)?); -// Ok((id, Self::dec(s))) -// } + let id = ReqId(u32::dec(s)?); + Ok((id, Self::dec(s)?)) + } -// /// Decode a request. -// /// -// /// Used by a SFTP server. Does not include the length field. -// pub fn decode_request(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> { -// let num = SftpNum::try_from(u8::dec(s)?)?; + /// Decode a request. + /// + /// Used by a SFTP server. Does not include the length field. + pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> + where + S: SSHSource<'de>, + 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes + 'de: 'a + { + let num = SftpNum::from(u8::dec(s)?); -// if !num.is_request() { -// return error::SSHProto.fail(); -// } + if !num.is_request() { + return Err(WireError::PacketWrong) + // return error::SSHProto.fail(); + // TODO: Not an error in the SSHProtocol rather the SFTP. + // Maybe is time to define an SftpError + } -// let id = ReqId(u32::dec(s)?); -// Ok((id, Self::dec(s))) -// } + let id = ReqId(u32::dec(s)?); + Ok((id, Self::dec(s)?)) + } -// /// Encode a response. -// /// -// /// Used by a SFTP server. Does not include the length field. -// pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> { -// if !self.sftp_num().is_response() { -// return Err(Error::bug()) -// } + /// Encode a response. + /// + /// Used by a SFTP server. Does not include the length field. + pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { -// // packet type -// self.sftp_num().enc(s)?; -// // request ID -// id.0.enc(s)?; -// // contents -// self.enc(s) -// } -// } + if !self.sftp_num().is_response() { + return Err(WireError::PacketWrong) + // return Err(Error::bug()) + // TODO: I understand that it would be a bad call of encode_response and + // therefore a bug, bug Error::bug() is not compatible with WireResult + } + + // packet type + self.sftp_num().enc(s)?; + // request ID + id.0.enc(s)?; + // contents + self.enc(s) + } } @@ -533,5 +556,4 @@ sftpmessages![ (102, Handle, Handle<'a>, SSH_FXP_HANDLE), (103, Data, Data<'a>, SSH_FXP_DATA), (104, Name, Name<'a>, SSH_FXP_NAME), - ]; From 2397aecd91f6b7262d65ac2f46ebd44778d536ea Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 28 Aug 2025 14:47:19 +1000 Subject: [PATCH 242/393] Fixing Avoidable warnings: - visibility for `FileHandle` - removing unused uses - removing unused local result and error --- sftp/src/proto.rs | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 5cc74343..8623de48 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,8 +1,5 @@ use num_enum::FromPrimitive; use paste::paste; -use sunset::error; -use sunset::error::Error as SunsetError; -use sunset::packets::{MessageNumber, Packet, Unknown}; use sunset::sshwire::{ BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, WireResult, @@ -15,7 +12,7 @@ use sunset_sshwire_derive::{SSHDecode, SSHEncode}; pub struct Filename<'a>(TextString<'a>); #[derive(Debug, SSHEncode, SSHDecode)] -struct FileHandle<'a>(pub BinString<'a>); +pub struct FileHandle<'a>(pub BinString<'a>); /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 const SFTP_VERSION: u32 = 3; @@ -296,25 +293,6 @@ impl<'de> SSHDecode<'de> for Attrs { } } -#[derive(Debug)] -pub enum Error { - UnknownPacket { number: u8 }, -} - -pub type Result = core::result::Result; - -// impl From for SunsetError { -// fn from(error: Error) -> SunsetError { -// SunsetError::Custom { -// msg: match error { -// Error::UnknownPacket { number } => { -// format_args!("Unknown SFTP packet: {}", number) -// } -// }, -// } -// } -// } - macro_rules! sftpmessages { ( $( ( $message_num:tt, From 40c3a9e74a7aad46add01b777f57dab860ddc79a Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 28 Aug 2025 15:07:00 +1000 Subject: [PATCH 243/393] removing outdated TODOs in sftp/src/proto.rs --- sftp/src/proto.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 8623de48..c285cb5b 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -286,9 +286,6 @@ impl<'de> SSHDecode<'de> for Attrs { // TODO: Implement extensions // if flags & AttrsFlags::SSH_FILEXFER_ATTR_EXTENDED != 0{ - // todo!("Not implemented"); - // } - Ok(attrs) } } @@ -331,7 +328,7 @@ macro_rules! sftpmessages { fn from(sftp_num: SftpNum) -> u8 { match sftp_num { $( - SftpNum::$SSH_FXP_NAME => $message_num, // TODO: Fix this + SftpNum::$SSH_FXP_NAME => $message_num, )* _ => 0 // Other, not in the enum definition @@ -425,7 +422,6 @@ macro_rules! sftpmessages { /// /// Used by a SFTP client. Does not include the length field. pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { - // TODO: handle the Error conversion if !self.sftp_num().is_request() { return Err(WireError::PacketWrong) // return Err(Error::bug()) From 9e46db465c5f6dceb9758c30e1bad037b5eb37a6 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 29 Aug 2025 11:28:12 +1000 Subject: [PATCH 244/393] Fix from comment @mkj comment on Name Fixed according to the indications in the next discussion: https://github.com/mkj/sunset/pull/29#discussion_r2306088167 Adding the expanded version for reference too Thanks --- sftp/out/cargo-expand-sftp.rs | 36 ++++++++--------------------------- sftp/src/proto.rs | 9 ++++++--- 2 files changed, 14 insertions(+), 31 deletions(-) diff --git a/sftp/out/cargo-expand-sftp.rs b/sftp/out/cargo-expand-sftp.rs index 0c712792..c1ea92d2 100644 --- a/sftp/out/cargo-expand-sftp.rs +++ b/sftp/out/cargo-expand-sftp.rs @@ -6,9 +6,6 @@ extern crate std; mod proto { use num_enum::FromPrimitive; use paste::paste; - use sunset::error; - use sunset::error::Error as SunsetError; - use sunset::packets::{MessageNumber, Packet, Unknown}; use sunset::sshwire::{ BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, WireResult, @@ -41,7 +38,7 @@ mod proto { Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) } } - struct FileHandle<'a>(pub BinString<'a>); + pub struct FileHandle<'a>(pub BinString<'a>); #[automatically_derived] impl<'a> ::core::fmt::Debug for FileHandle<'a> { #[inline] @@ -502,15 +499,18 @@ mod proto { }) } } - pub struct Name<'de>(pub Vec>); + pub struct Name<'a>(pub Vec>); #[automatically_derived] - impl<'de> ::core::fmt::Debug for Name<'de> { + impl<'a> ::core::fmt::Debug for Name<'a> { #[inline] fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Name", &&self.0) } } - impl<'de> SSHDecode<'de> for Name<'de> { + impl<'a: 'de, 'de> SSHDecode<'de> for Name<'a> + where + 'de: 'a, + { fn dec(s: &mut S) -> WireResult where S: SSHSource<'de>, @@ -523,7 +523,7 @@ mod proto { Ok(Name(names)) } } - impl SSHEncode for Name<'_> { + impl<'a> SSHEncode for Name<'a> { fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { (self.0.len() as u32).enc(s)?; for element in self.0.iter() { @@ -930,26 +930,6 @@ mod proto { Ok(attrs) } } - pub enum Error { - UnknownPacket { number: u8 }, - } - #[automatically_derived] - impl ::core::fmt::Debug for Error { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - match self { - Error::UnknownPacket { number: __self_0 } => { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "UnknownPacket", - "number", - &__self_0, - ) - } - } - } - } - pub type Result = core::result::Result; #[repr(u8)] #[allow(non_camel_case_types)] pub enum SftpNum { diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index c285cb5b..d0657250 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -90,9 +90,12 @@ pub struct NameEntry<'a> { } #[derive(Debug)] -pub struct Name<'de>(pub Vec>); +pub struct Name<'a>(pub Vec>); -impl<'de> SSHDecode<'de> for Name<'de> { +impl<'a: 'de, 'de> SSHDecode<'de> for Name<'a> +where + 'de: 'a, +{ fn dec(s: &mut S) -> WireResult where S: SSHSource<'de>, @@ -109,7 +112,7 @@ impl<'de> SSHDecode<'de> for Name<'de> { } } -impl SSHEncode for Name<'_> { +impl<'a> SSHEncode for Name<'a> { fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { (self.0.len() as u32).enc(s)?; From a805693e7a88e00c6aba86f517306d00b58873ba Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Sat, 30 Aug 2025 12:25:44 +1000 Subject: [PATCH 245/393] reordering dependencies in sftp to match other workspace members --- sftp/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index b875c003..2826346c 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -4,7 +4,8 @@ version = "0.1.0" edition = "2024" [dependencies] -num_enum = {version = "0.7.4"} sunset = { version = "0.3.0", path = "../" } sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } + +num_enum = {version = "0.7.4"} paste = "1.0" \ No newline at end of file From eecadde5fb2ce3ec2f45ba52f8df3e0ed7a20a9c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Sat, 30 Aug 2025 12:29:56 +1000 Subject: [PATCH 246/393] Starting a new sftp-std demo taking the basic std demo as model It has some very basic code proving that when an sftp connection is accepted the version handshake starts SSH_FXP_INIT -> <- SSH_FXP_VERSION etc --- Cargo.lock | 24 ++++ Cargo.toml | 1 + demo/sftp/std/Cargo.toml | 38 ++++++ demo/sftp/std/debug_sftp_client.sh | 3 + demo/sftp/std/rust-toolchain.toml | 3 + demo/sftp/std/src/main.rs | 192 +++++++++++++++++++++++++++++ demo/sftp/std/tap.sh | 7 ++ 7 files changed, 268 insertions(+) create mode 100644 demo/sftp/std/Cargo.toml create mode 100755 demo/sftp/std/debug_sftp_client.sh create mode 100644 demo/sftp/std/rust-toolchain.toml create mode 100644 demo/sftp/std/src/main.rs create mode 100755 demo/sftp/std/tap.sh diff --git a/Cargo.lock b/Cargo.lock index 8dcaffa8..5def08c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2849,6 +2849,30 @@ dependencies = [ "sunset-sshwire-derive", ] +[[package]] +name = "sunset-demo-sftp-std" +version = "0.1.0" +dependencies = [ + "async-io", + "critical-section", + "embassy-executor", + "embassy-futures", + "embassy-net", + "embassy-net-tuntap", + "embassy-sync 0.7.0", + "embassy-time", + "embedded-io-async", + "env_logger", + "heapless", + "libc", + "log", + "rand", + "sha2", + "sunset", + "sunset-async", + "sunset-demo-common", +] + [[package]] name = "sunset-demo-std" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 4e0c6627..c415ba01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "demo/picow", "demo/std", "fuzz", "sftp", + "demo/sftp/std", "stdasync", # workspace.dependencies paths are automatic ] diff --git a/demo/sftp/std/Cargo.toml b/demo/sftp/std/Cargo.toml new file mode 100644 index 00000000..da11494c --- /dev/null +++ b/demo/sftp/std/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "sunset-demo-sftp-std" +version = "0.1.0" +edition = "2021" + +[dependencies] +sunset = { workspace = true, features = ["rsa", "std"] } +sunset-async.workspace = true +sunset-demo-common.workspace = true + +# 131072 was determined empirically +embassy-executor = { version = "0.7", features = [ + "executor-thread", "arch-std", "log", "task-arena-size-131072"] } +embassy-net = { version = "0.7", features = ["tcp", "dhcpv4", "medium-ethernet"] } +embassy-net-tuntap = { version = "0.1" } +embassy-sync = { version = "0.7" } +embassy-futures = { version = "0.1" } +# embassy-time dep required to link a time driver +embassy-time = { version = "0.4", default-features=false, features = ["log", "std"] } + +log = { version = "0.4" } +# default regex feature is huge +env_logger = { version = "0.11", default-features=false, features = ["auto-color", "humantime"] } + +embedded-io-async = "0.6" +heapless = "0.8" + +# for tuntap +libc = "0.2.101" +async-io = "1.6.0" + +# using local fork +# menu = "0.3" + + +critical-section = "1.1" +rand = { version = "0.8", default-features = false, features = ["getrandom"] } +sha2 = { version = "0.10", default-features = false } diff --git a/demo/sftp/std/debug_sftp_client.sh b/demo/sftp/std/debug_sftp_client.sh new file mode 100755 index 00000000..a2e5e4ee --- /dev/null +++ b/demo/sftp/std/debug_sftp_client.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR any@192.168.69.2 \ No newline at end of file diff --git a/demo/sftp/std/rust-toolchain.toml b/demo/sftp/std/rust-toolchain.toml new file mode 100644 index 00000000..9993e936 --- /dev/null +++ b/demo/sftp/std/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = [ "rustfmt" ] diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs new file mode 100644 index 00000000..2e43fda2 --- /dev/null +++ b/demo/sftp/std/src/main.rs @@ -0,0 +1,192 @@ +use embedded_io_async::{Read, Write}; +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; + +use embassy_executor::Spawner; +use embassy_net::{Stack, StackResources, StaticConfigV4}; + +use rand::rngs::OsRng; +use rand::RngCore; + +use embassy_futures::select::select; +use embassy_net_tuntap::TunTapDevice; +use embassy_sync::channel::Channel; + +use sunset::*; +use sunset_async::{ProgressHolder, SSHServer, SunsetMutex, SunsetRawMutex}; + +pub(crate) use sunset_demo_common as demo_common; + +use demo_common::{DemoCommon, DemoServer, SSHConfig}; + +const NUM_LISTENERS: usize = 4; +// +1 for dhcp +const NUM_SOCKETS: usize = NUM_LISTENERS + 1; + +#[embassy_executor::task] +async fn net_task(mut runner: embassy_net::Runner<'static, TunTapDevice>) -> ! { + runner.run().await +} + +#[embassy_executor::task] +async fn main_task(spawner: Spawner) { + // TODO config + let opt_tap0 = "tap0"; + let ip4 = "192.168.69.2"; + let cir = 24; + + let config = Box::leak(Box::new({ + let mut config = SSHConfig::new().unwrap(); + config.set_admin_pw(Some("pw")).unwrap(); + config.console_noauth = true; + config.ip4_static = if let Ok(ip) = ip4.parse() { + Some(StaticConfigV4 { + address: embassy_net::Ipv4Cidr::new(ip, cir), + gateway: None, + dns_servers: { heapless::Vec::new() }, + }) + } else { + None + }; + SunsetMutex::new(config) + })); + + let net_cf = if let Some(ref s) = config.lock().await.ip4_static { + embassy_net::Config::ipv4_static(s.clone()) + } else { + embassy_net::Config::dhcpv4(Default::default()) + }; + info!("Net config: {net_cf:?}"); + + // Init network device + let net_device = TunTapDevice::new(opt_tap0).unwrap(); + + let seed = OsRng.next_u64(); + + // Init network stack + let res = Box::leak(Box::new(StackResources::::new())); + let (stack, runner) = embassy_net::new(net_device, net_cf, res, seed); + + // Launch network task + spawner.spawn(net_task(runner)).unwrap(); + + for _ in 0..NUM_LISTENERS { + spawner.spawn(listen(stack, config)).unwrap(); + } +} + +#[derive(Default)] +struct StdDemo; + +impl DemoServer for StdDemo { + async fn run(&self, serv: &SSHServer<'_>, mut common: DemoCommon) -> Result<()> { + let chan_pipe = Channel::::new(); + + let prog_loop = async { + loop { + let mut ph = ProgressHolder::new(); + let ev = serv.progress(&mut ph).await?; + trace!("ev {ev:?}"); + match ev { + ServEvent::SessionShell(a) => { + a.fail()?; // Not allowed in this example, kept here for compatibility + } + ServEvent::SessionExec(a) => { + a.fail()?; // Not allowed in this example, kept here for compatibility + } + ServEvent::SessionSubsystem(a) => { + match a.command()?.to_lowercase().as_str() { + "sftp" => { + info!("Starting '{}' subsystem", a.command()?); + + if let Some(ch) = common.sess.take() { + debug_assert!(ch.num() == a.channel()); + a.succeed()?; + let _ = chan_pipe.try_send(ch); + } else { + a.fail()?; + } + } + _ => { + warn!( + "request for subsystem '{}' not implemented: fail", + a.command()? + ); + a.fail()?; + } + } + } + other => common.handle_event(other)?, + }; + } + #[allow(unreachable_code)] + Ok::<_, Error>(()) + }; + + let prog_loop = async { + if let Err(e) = prog_loop.await { + warn!("Exited: {e:?}"); + } + }; + + let sftp_loop = async { + let ch = chan_pipe.receive().await; + info!("This is the sftp loop"); + // After this you are likely to receive an SSH_FXP_INIT + let mut stdio = serv.stdio(ch).await?; + + loop { + let mut b = [0u8; 200]; + let lr = stdio.read(&mut b).await?; + if lr == 0 { + break; + } + let b = &mut b[..lr]; + info!("received '{:?}'", b); + + // First packet received! [0, 0, 0, 5, 1, 0, 0, 0, 3], meaning: + // Len (u32): [0, 0, 0, 5] 5 bytes + // Type (u8): [1] SSH_FXP_INIT + // Version (u32): [0, 0, 0, 3]] version 3 + // So we need to answer 5 SSH_FXP_VERSION 3 + // [0, 0, 0, 5, 2, 0, 0, 0, 3] + let hardcoded_version_r: Vec = vec![0, 0, 0, 5, 2, 0, 0, 0, 3]; + info!("sending '{:?}'", hardcoded_version_r); + + stdio.write(hardcoded_version_r.as_slice()).await?; + } + + Ok::<_, Error>(()) + }; + + select(prog_loop, sftp_loop).await; + todo!() + } +} + +// TODO: pool_size should be NUM_LISTENERS but needs a literal +#[embassy_executor::task(pool_size = 4)] +async fn listen( + stack: Stack<'static>, + config: &'static SunsetMutex, +) -> ! { + let demo = StdDemo::default(); + demo_common::listen(stack, config, &demo).await +} + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + env_logger::builder() + .filter_level(log::LevelFilter::Trace) + // .filter_module("sunset::runner", log::LevelFilter::Info) + .filter_module("sunset::traffic", log::LevelFilter::Info) + .filter_module("sunset::encrypt", log::LevelFilter::Info) + // .filter_module("sunset::conn", log::LevelFilter::Info) + // .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) + .filter_module("async_io", log::LevelFilter::Info) + .filter_module("polling", log::LevelFilter::Info) + .format_timestamp_nanos() + .init(); + + spawner.spawn(main_task(spawner)).unwrap(); +} diff --git a/demo/sftp/std/tap.sh b/demo/sftp/std/tap.sh new file mode 100755 index 00000000..8732a0cd --- /dev/null +++ b/demo/sftp/std/tap.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# This script generates the tap device that the demo will bind the network stack +# usage `sudo ./tap.sh` + +ip tuntap add name tap0 mode tap user $SUDO_USER group $SUDO_USER +ip addr add 192.168.69.100/24 dev tap0 +ip link set tap0 up From 72f5f0ec8b8e6b22f45109a01faf056745a368b5 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Sat, 30 Aug 2025 12:30:27 +1000 Subject: [PATCH 247/393] adding mod sftpserver as I am going to start working on it next --- sftp/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 0368d938..4c2f3ffe 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,2 +1,2 @@ mod proto; -// mod sftpserver; +mod sftpserver; From 109d022a38f87d4e9c3b499703e5152e8bc9de63 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 3 Sep 2025 13:55:40 +1000 Subject: [PATCH 248/393] Adding PathInfo SFTP Packet as it is requested by sftp clients after initialisation --- sftp/src/proto.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index d0657250..042d0623 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -61,6 +61,11 @@ pub struct Write<'a> { // Responses +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct PathInfo<'a> { + pub path: TextString<'a>, +} + #[derive(Debug, SSHEncode, SSHDecode)] pub struct Status<'a> { pub code: StatusCode, From 60ef17b220acd9056042533d5c02668c22196e49 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 3 Sep 2025 13:57:10 +1000 Subject: [PATCH 249/393] adding some Doc comments --- sftp/src/proto.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 042d0623..0cc661fb 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -180,6 +180,7 @@ pub struct ExtPair<'a> { pub data: BinString<'a>, } +/// Files attributes to describe Files as SFTP v3 specification #[derive(Debug, Default)] pub struct Attrs { // flags: u32, defines used attributes @@ -308,6 +309,7 @@ macro_rules! sftpmessages { )* ) => { paste! { + /// Represent a subset of the SFTP packet types defined by draft-ietf-secsh-filexfer-02 #[derive(Debug, Clone, FromPrimitive, SSHEncode)] #[repr(u8)] #[allow(non_camel_case_types)] From ba280074705b2266fa97c233b868d43a54f7f381 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 3 Sep 2025 13:58:25 +1000 Subject: [PATCH 250/393] exposing sftp entities in lib.rs --- sftp/src/lib.rs | 10 ++++++++++ sftp/src/proto.rs | 2 +- sftp/src/sftpserver.rs | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 4c2f3ffe..ae003bca 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,2 +1,12 @@ mod proto; mod sftpserver; + +pub use sftpserver::DirReply; +pub use sftpserver::ReadReply; +pub use sftpserver::Result; +pub use sftpserver::SftpServer; + +pub use proto::Attrs; +pub use proto::SFTP_VERSION; +pub use proto::SftpNum; +pub use proto::SftpPacket; diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 0cc661fb..40473041 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -15,7 +15,7 @@ pub struct Filename<'a>(TextString<'a>); pub struct FileHandle<'a>(pub BinString<'a>); /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 -const SFTP_VERSION: u32 = 3; +pub const SFTP_VERSION: u32 = 3; /// The SFTP version of the client #[derive(Debug, SSHEncode, SSHDecode)] diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 8a6ce16f..bbee31de 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -7,7 +7,7 @@ pub type Result = core::result::Result; /// Some less core operations have a Provided implementation returning /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. -trait SftpServer { +pub trait SftpServer { type Handle; // TODO flags struct From b3aeff6b5cdd779ff7b8dadb5fd30001a6905cbe Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 3 Sep 2025 14:03:01 +1000 Subject: [PATCH 251/393] fixing impl From for u8 captures the number in Other SftpNum --- sftp/src/proto.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 40473041..cfb63a1a 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -340,7 +340,8 @@ macro_rules! sftpmessages { $( SftpNum::$SSH_FXP_NAME => $message_num, )* - _ => 0 // Other, not in the enum definition + + SftpNum::Other(number) => number // Other, not in the enum definition } } From b92de8919c60066006cf2db94674a61cfd7f1272 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 3 Sep 2025 20:35:49 +1000 Subject: [PATCH 252/393] Added log to sftp --- sftp/Cargo.toml | 3 ++- sftp/src/proto.rs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 2826346c..2deb566d 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -8,4 +8,5 @@ sunset = { version = "0.3.0", path = "../" } sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } num_enum = {version = "0.7.4"} -paste = "1.0" \ No newline at end of file +paste = "1.0" +log = "0.4" diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index cfb63a1a..b42c1c87 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -7,6 +7,8 @@ use sunset::sshwire::{ use sunset_sshwire_derive::{SSHDecode, SSHEncode}; +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; // TODO is utf8 enough, or does this need to be an opaque binstring? #[derive(Debug, SSHEncode, SSHDecode)] pub struct Filename<'a>(TextString<'a>); From 464c9f7b2ae3710ef6a1de5c77d9dc28e304511b Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 3 Sep 2025 20:45:31 +1000 Subject: [PATCH 253/393] Big changes: Fixed decoding SftpPacket This changes the sftpmessge parameters to handle the three classes of SFTP Packets: Initialization, Requests and Responses. The motivation is to add a mechanism to capture the Request Ids. Since only requests and responses contain that field but they do not include in their "inner" sftp Packets definitions (See Read, Write, etc) I thought that it was a good idea keeping them out of them. To store them away from the Inner packets I decided adding extra parameters to the SftpPacket enum. To do so the parameters have been reestructured so each class of sftp packet can be expanded with the required data. As a side effect, the sftpmessages packets definition readability has been improved. Certainly this adds complexity to the sftpmessages --- sftp/out/cargo-expand-sftp.rs | 507 +++++++++++++++++++++++++++++----- sftp/src/proto.rs | 261 +++++++++++++---- 2 files changed, 635 insertions(+), 133 deletions(-) diff --git a/sftp/out/cargo-expand-sftp.rs b/sftp/out/cargo-expand-sftp.rs index c1ea92d2..0032029e 100644 --- a/sftp/out/cargo-expand-sftp.rs +++ b/sftp/out/cargo-expand-sftp.rs @@ -11,6 +11,8 @@ mod proto { WireResult, }; use sunset_sshwire_derive::{SSHDecode, SSHEncode}; + #[allow(unused_imports)] + use log::{debug, error, info, log, trace, warn}; pub struct Filename<'a>(TextString<'a>); #[automatically_derived] impl<'a> ::core::fmt::Debug for Filename<'a> { @@ -66,7 +68,7 @@ mod proto { } } /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 - const SFTP_VERSION: u32 = 3; + pub const SFTP_VERSION: u32 = 3; /// The SFTP version of the client pub struct InitVersionClient { pub version: u32, @@ -315,6 +317,41 @@ mod proto { }) } } + pub struct PathInfo<'a> { + pub path: TextString<'a>, + } + #[automatically_derived] + impl<'a> ::core::fmt::Debug for PathInfo<'a> { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "PathInfo", + "path", + &&self.path, + ) + } + } + impl<'a> ::sunset::sshwire::SSHEncode for PathInfo<'a> { + fn enc( + &self, + s: &mut dyn ::sunset::sshwire::SSHSink, + ) -> ::sunset::sshwire::WireResult<()> { + ::sunset::sshwire::SSHEncode::enc(&self.path, s)?; + Ok(()) + } + } + impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for PathInfo<'a> + where + 'de: 'a, + { + fn dec>( + s: &mut S, + ) -> ::sunset::sshwire::WireResult { + let field_path = ::sunset::sshwire::SSHDecode::dec(s)?; + Ok(Self { path: field_path }) + } + } pub struct Status<'a> { pub code: StatusCode, pub message: TextString<'a>, @@ -795,6 +832,7 @@ mod proto { }) } } + /// Files attributes to describe Files as SFTP v3 specification pub struct Attrs { pub size: Option, pub uid: Option, @@ -930,6 +968,7 @@ mod proto { Ok(attrs) } } + /// Represent a subset of the SFTP packet types defined by draft-ietf-secsh-filexfer-02 #[repr(u8)] #[allow(non_camel_case_types)] pub enum SftpNum { @@ -945,6 +984,8 @@ mod proto { SSH_FXP_READ = 5, #[sshwire(variant = "ssh_fxp_write")] SSH_FXP_WRITE = 6, + #[sshwire(variant = "ssh_fxp_realpath")] + SSH_FXP_REALPATH = 16, #[sshwire(variant = "ssh_fxp_status")] SSH_FXP_STATUS = 101, #[sshwire(variant = "ssh_fxp_handle")] @@ -981,6 +1022,9 @@ mod proto { SftpNum::SSH_FXP_WRITE => { ::core::fmt::Formatter::write_str(f, "SSH_FXP_WRITE") } + SftpNum::SSH_FXP_REALPATH => { + ::core::fmt::Formatter::write_str(f, "SSH_FXP_REALPATH") + } SftpNum::SSH_FXP_STATUS => { ::core::fmt::Formatter::write_str(f, "SSH_FXP_STATUS") } @@ -1015,6 +1059,7 @@ mod proto { SftpNum::SSH_FXP_CLOSE => SftpNum::SSH_FXP_CLOSE, SftpNum::SSH_FXP_READ => SftpNum::SSH_FXP_READ, SftpNum::SSH_FXP_WRITE => SftpNum::SSH_FXP_WRITE, + SftpNum::SSH_FXP_REALPATH => SftpNum::SSH_FXP_REALPATH, SftpNum::SSH_FXP_STATUS => SftpNum::SSH_FXP_STATUS, SftpNum::SSH_FXP_HANDLE => SftpNum::SSH_FXP_HANDLE, SftpNum::SSH_FXP_DATA => SftpNum::SSH_FXP_DATA, @@ -1035,6 +1080,7 @@ mod proto { const SSH_FXP_CLOSE__num_enum_0__: u8 = 4; const SSH_FXP_READ__num_enum_0__: u8 = 5; const SSH_FXP_WRITE__num_enum_0__: u8 = 6; + const SSH_FXP_REALPATH__num_enum_0__: u8 = 16; const SSH_FXP_STATUS__num_enum_0__: u8 = 101; const SSH_FXP_HANDLE__num_enum_0__: u8 = 102; const SSH_FXP_DATA__num_enum_0__: u8 = 103; @@ -1047,6 +1093,7 @@ mod proto { SSH_FXP_CLOSE__num_enum_0__ => Self::SSH_FXP_CLOSE, SSH_FXP_READ__num_enum_0__ => Self::SSH_FXP_READ, SSH_FXP_WRITE__num_enum_0__ => Self::SSH_FXP_WRITE, + SSH_FXP_REALPATH__num_enum_0__ => Self::SSH_FXP_REALPATH, SSH_FXP_STATUS__num_enum_0__ => Self::SSH_FXP_STATUS, SSH_FXP_HANDLE__num_enum_0__ => Self::SSH_FXP_HANDLE, SSH_FXP_DATA__num_enum_0__ => Self::SSH_FXP_DATA, @@ -1076,6 +1123,7 @@ mod proto { Self::SSH_FXP_CLOSE => {} Self::SSH_FXP_READ => {} Self::SSH_FXP_WRITE => {} + Self::SSH_FXP_REALPATH => {} Self::SSH_FXP_STATUS => {} Self::SSH_FXP_HANDLE => {} Self::SSH_FXP_DATA => {} @@ -1096,6 +1144,7 @@ mod proto { Self::SSH_FXP_CLOSE => "ssh_fxp_close", Self::SSH_FXP_READ => "ssh_fxp_read", Self::SSH_FXP_WRITE => "ssh_fxp_write", + Self::SSH_FXP_REALPATH => "ssh_fxp_realpath", Self::SSH_FXP_STATUS => "ssh_fxp_status", Self::SSH_FXP_HANDLE => "ssh_fxp_handle", Self::SSH_FXP_DATA => "ssh_fxp_data", @@ -1124,15 +1173,19 @@ mod proto { SftpNum::SSH_FXP_CLOSE => 4, SftpNum::SSH_FXP_READ => 5, SftpNum::SSH_FXP_WRITE => 6, + SftpNum::SSH_FXP_REALPATH => 16, SftpNum::SSH_FXP_STATUS => 101, SftpNum::SSH_FXP_HANDLE => 102, SftpNum::SSH_FXP_DATA => 103, SftpNum::SSH_FXP_NAME => 104, - _ => 0, + SftpNum::Other(number) => number, } } } impl SftpNum { + fn is_init(&self) -> bool { + (1..=1).contains(&(u8::from(self.clone()))) + } fn is_request(&self) -> bool { (2..=99).contains(&(u8::from(self.clone()))) } @@ -1147,14 +1200,15 @@ mod proto { pub enum SftpPacket<'a> { Init(InitVersionClient), Version(InitVersionLowest), - Open(Open<'a>), - Close(Close<'a>), - Read(Read<'a>), - Write(Write<'a>), - Status(Status<'a>), - Handle(Handle<'a>), - Data(Data<'a>), - Name(Name<'a>), + Open(ReqId, Open<'a>), + Close(ReqId, Close<'a>), + Read(ReqId, Read<'a>), + Write(ReqId, Write<'a>), + PathInfo(ReqId, PathInfo<'a>), + Status(ReqId, Status<'a>), + Handle(ReqId, Handle<'a>), + Data(ReqId, Data<'a>), + Name(ReqId, Name<'a>), } #[automatically_derived] impl<'a> ::core::fmt::Debug for SftpPacket<'a> { @@ -1175,60 +1229,76 @@ mod proto { &__self_0, ) } - SftpPacket::Open(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::Open(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Open", - &__self_0, + __self_0, + &__self_1, ) } - SftpPacket::Close(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::Close(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Close", - &__self_0, + __self_0, + &__self_1, ) } - SftpPacket::Read(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::Read(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Read", - &__self_0, + __self_0, + &__self_1, ) } - SftpPacket::Write(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::Write(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Write", - &__self_0, + __self_0, + &__self_1, ) } - SftpPacket::Status(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::PathInfo(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( + f, + "PathInfo", + __self_0, + &__self_1, + ) + } + SftpPacket::Status(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Status", - &__self_0, + __self_0, + &__self_1, ) } - SftpPacket::Handle(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::Handle(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Handle", - &__self_0, + __self_0, + &__self_1, ) } - SftpPacket::Data(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::Data(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Data", - &__self_0, + __self_0, + &__self_1, ) } - SftpPacket::Name(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( + SftpPacket::Name(__self_0, __self_1) => { + ::core::fmt::Formatter::debug_tuple_field2_finish( f, "Name", - &__self_0, + __self_0, + &__self_1, ) } } @@ -1241,14 +1311,42 @@ mod proto { match self { SftpPacket::Init(p) => p.enc(s)?, SftpPacket::Version(p) => p.enc(s)?, - SftpPacket::Open(p) => p.enc(s)?, - SftpPacket::Close(p) => p.enc(s)?, - SftpPacket::Read(p) => p.enc(s)?, - SftpPacket::Write(p) => p.enc(s)?, - SftpPacket::Status(p) => p.enc(s)?, - SftpPacket::Handle(p) => p.enc(s)?, - SftpPacket::Data(p) => p.enc(s)?, - SftpPacket::Name(p) => p.enc(s)?, + SftpPacket::Open(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::Close(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::Read(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::Write(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::PathInfo(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::Status(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::Handle(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::Data(id, p) => { + id.enc(s)?; + p.enc(s)? + } + SftpPacket::Name(id, p) => { + id.enc(s)?; + p.enc(s)? + } }; Ok(()) } @@ -1273,36 +1371,49 @@ mod proto { SftpPacket::Version(inner_type) } SftpNum::SSH_FXP_OPEN => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Open(inner_type) + SftpPacket::Open(req_id, inner_type) } SftpNum::SSH_FXP_CLOSE => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Close(inner_type) + SftpPacket::Close(req_id, inner_type) } SftpNum::SSH_FXP_READ => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Read(inner_type) + SftpPacket::Read(req_id, inner_type) } SftpNum::SSH_FXP_WRITE => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Write(inner_type) + SftpPacket::Write(req_id, inner_type) + } + SftpNum::SSH_FXP_REALPATH => { + let req_id = ::dec(s)?; + let inner_type = >::dec(s)?; + SftpPacket::PathInfo(req_id, inner_type) } SftpNum::SSH_FXP_STATUS => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Status(inner_type) + SftpPacket::Status(req_id, inner_type) } SftpNum::SSH_FXP_HANDLE => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Handle(inner_type) + SftpPacket::Handle(req_id, inner_type) } SftpNum::SSH_FXP_DATA => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Data(inner_type) + SftpPacket::Data(req_id, inner_type) } SftpNum::SSH_FXP_NAME => { + let req_id = ::dec(s)?; let inner_type = >::dec(s)?; - SftpPacket::Name(inner_type) + SftpPacket::Name(req_id, inner_type) } _ => { return Err(WireError::UnknownPacket { @@ -1319,14 +1430,15 @@ mod proto { match self { SftpPacket::Init(_) => SftpNum::from(1 as u8), SftpPacket::Version(_) => SftpNum::from(2 as u8), - SftpPacket::Open(_) => SftpNum::from(3 as u8), - SftpPacket::Close(_) => SftpNum::from(4 as u8), - SftpPacket::Read(_) => SftpNum::from(5 as u8), - SftpPacket::Write(_) => SftpNum::from(6 as u8), - SftpPacket::Status(_) => SftpNum::from(101 as u8), - SftpPacket::Handle(_) => SftpNum::from(102 as u8), - SftpPacket::Data(_) => SftpNum::from(103 as u8), - SftpPacket::Name(_) => SftpNum::from(104 as u8), + SftpPacket::Open(_, _) => SftpNum::from(3 as u8), + SftpPacket::Close(_, _) => SftpNum::from(4 as u8), + SftpPacket::Read(_, _) => SftpNum::from(5 as u8), + SftpPacket::Write(_, _) => SftpNum::from(6 as u8), + SftpPacket::PathInfo(_, _) => SftpNum::from(16 as u8), + SftpPacket::Status(_, _) => SftpNum::from(101 as u8), + SftpPacket::Handle(_, _) => SftpNum::from(102 as u8), + SftpPacket::Data(_, _) => SftpNum::from(103 as u8), + SftpPacket::Name(_, _) => SftpNum::from(104 as u8), } } /// Encode a request. @@ -1356,21 +1468,22 @@ mod proto { let id = ReqId(u32::dec(s)?); Ok((id, Self::dec(s)?)) } - /// Decode a request. + /// Decode a request. Includes Init /// - /// Used by a SFTP server. Does not include the length field. - pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> + /// Used by a SFTP server. Does not include the length field. If the request does not have id (Initialisation) + pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(Option, Self)> where S: SSHSource<'de>, 'a: 'de, 'de: 'a, { let num = SftpNum::from(u8::dec(s)?); - if !num.is_request() { + if (!num.is_request() && !num.is_init()) { return Err(WireError::PacketWrong); } - let id = ReqId(u32::dec(s)?); - Ok((id, Self::dec(s)?)) + let maybe_id = if num.is_init() { None } else { Some(ReqId(u32::dec(s)?)) }; + let sftp_packet = Self::dec(s)?; + Ok((maybe_id, sftp_packet)) } /// Encode a response. /// @@ -1394,44 +1507,294 @@ mod proto { SftpPacket::Version(s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Open<'a>) -> SftpPacket<'a> { - SftpPacket::Open(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_open", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Open(ReqId(0), s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Close<'a>) -> SftpPacket<'a> { - SftpPacket::Close(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_close", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Close(ReqId(0), s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Read<'a>) -> SftpPacket<'a> { - SftpPacket::Read(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_read", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Read(ReqId(0), s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Write<'a>) -> SftpPacket<'a> { - SftpPacket::Write(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_write", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Write(ReqId(0), s) + } + } + /// **Warning**: No Sequence Id can be infered from a Packet Type + impl<'a> From> for SftpPacket<'a> { + fn from(s: PathInfo<'a>) -> SftpPacket<'a> { + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_realpath", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::PathInfo(ReqId(0), s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Status<'a>) -> SftpPacket<'a> { - SftpPacket::Status(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_status", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Status(ReqId(0), s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Handle<'a>) -> SftpPacket<'a> { - SftpPacket::Handle(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_handle", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Handle(ReqId(0), s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Data<'a>) -> SftpPacket<'a> { - SftpPacket::Data(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_data", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Data(ReqId(0), s) } } + /// **Warning**: No Sequence Id can be infered from a Packet Type impl<'a> From> for SftpPacket<'a> { fn from(s: Name<'a>) -> SftpPacket<'a> { - SftpPacket::Name(s) + { + { + let lvl = ::log::Level::Warn; + if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { + ::log::__private_api::log( + { ::log::__private_api::GlobalLogger }, + format_args!( + "Casting from {0:?} to SftpPacket cannot set Request Id", + "ssh_fxp_name", + ), + lvl, + &( + "sunset_sftp::proto", + "sunset_sftp::proto", + ::log::__private_api::loc(), + ), + (), + ); + } + } + }; + SftpPacket::Name(ReqId(0), s) } } } +mod sftpserver { + use crate::proto::{Attrs, StatusCode}; + use core::marker::PhantomData; + pub type Result = core::result::Result; + /// All trait functions are optional in the SFTP protocol. + /// Some less core operations have a Provided implementation returning + /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, + /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. + pub trait SftpServer { + type Handle; + async fn open(filename: &str, flags: u32, attrs: &Attrs) -> Result; + /// Close either a file or directory handle + async fn close(handle: &Self::Handle) -> Result<()>; + async fn read( + handle: &Self::Handle, + offset: u64, + reply: &mut ReadReply, + ) -> Result<()>; + async fn write(handle: &Self::Handle, offset: u64, buf: &[u8]) -> Result<()>; + async fn opendir(dir: &str) -> Result; + async fn readdir(handle: &Self::Handle, reply: &mut DirReply) -> Result<()>; + } + pub struct ReadReply<'g, 'a> { + chan: ChanOut<'g, 'a>, + } + impl<'g, 'a> ReadReply<'g, 'a> { + pub async fn reply(self, data: &[u8]) {} + } + pub struct DirReply<'g, 'a> { + chan: ChanOut<'g, 'a>, + } + impl<'g, 'a> DirReply<'g, 'a> { + pub async fn reply(self, data: &[u8]) {} + } + pub struct ChanOut<'g, 'a> { + _phantom_g: PhantomData<&'g ()>, + _phantom_a: PhantomData<&'a ()>, + } +} +pub use sftpserver::DirReply; +pub use sftpserver::ReadReply; +pub use sftpserver::Result; +pub use sftpserver::SftpServer; +pub use proto::Attrs; +pub use proto::SFTP_VERSION; +pub use proto::SftpNum; +pub use proto::SftpPacket; diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index b42c1c87..4fb6f832 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -303,12 +303,30 @@ impl<'de> SSHDecode<'de> for Attrs { macro_rules! sftpmessages { ( - $( ( $message_num:tt, - $SpecificPacketVariant:ident, - $SpecificPacketType:ty, - $SSH_FXP_NAME:ident - ), - )* + init: { + $( ( $init_message_num:tt, + $init_packet_variant:ident, + $init_packet_type:ty, + $init_ssh_fxp_name:literal + ), + )* + }, + request: { + $( ( $request_message_num:tt, + $request_packet_variant:ident, + $request_packet_type:ty, + $request_ssh_fxp_name:literal + ), + )* + }, + response: { + $( ( $response_message_num:tt, + $response_packet_variant:ident, + $response_packet_type:ty, + $response_ssh_fxp_name:literal + ), + )* + }, ) => { paste! { /// Represent a subset of the SFTP packet types defined by draft-ietf-secsh-filexfer-02 @@ -316,14 +334,24 @@ macro_rules! sftpmessages { #[repr(u8)] #[allow(non_camel_case_types)] pub enum SftpNum { - // SSH_FXP_OPEN = 3, - $( - #[sshwire(variant = $SSH_FXP_NAME:lower)] - $SSH_FXP_NAME = $message_num, - )* - #[sshwire(unknown)] - #[num_enum(catch_all)] - Other(u8), + $( + #[sshwire(variant = $init_ssh_fxp_name)] + [<$init_ssh_fxp_name:upper>] = $init_message_num, + )* + + $( + #[sshwire(variant = $request_ssh_fxp_name)] + [<$request_ssh_fxp_name:upper>] = $request_message_num, + )* + + $( + #[sshwire(variant = $response_ssh_fxp_name)] + [<$response_ssh_fxp_name:upper>] = $response_message_num, + )* + + #[sshwire(unknown)] + #[num_enum(catch_all)] + Other(u8), } } // paste @@ -335,12 +363,18 @@ macro_rules! sftpmessages { Ok(SftpNum::from(u8::dec(s)?)) } } - + paste!{ impl From for u8{ fn from(sftp_num: SftpNum) -> u8 { match sftp_num { - $( - SftpNum::$SSH_FXP_NAME => $message_num, + $( + SftpNum::[<$init_ssh_fxp_name:upper>] => $init_message_num, + )* + $( + SftpNum::[<$request_ssh_fxp_name:upper>] => $request_message_num, + )* + $( + SftpNum::[<$response_ssh_fxp_name:upper>] => $response_message_num, )* SftpNum::Other(number) => number // Other, not in the enum definition @@ -350,7 +384,13 @@ macro_rules! sftpmessages { } + } //paste + impl SftpNum { + fn is_init(&self) -> bool { + (1..=1).contains(&(u8::from(self.clone()))) + } + fn is_request(&self) -> bool { // TODO SSH_FXP_EXTENDED (2..=99).contains(&(u8::from(self.clone()))) @@ -362,18 +402,26 @@ macro_rules! sftpmessages { } } + /// Top level SSH packet enum /// /// It helps identifying the SFTP Packet type and handling it accordingly /// This is done using the SFTP field type #[derive(Debug)] pub enum SftpPacket<'a> { - // eg Open(Open<'a>), - $( - $SpecificPacketVariant($SpecificPacketType), - )* + $( + $init_packet_variant($init_packet_type), + )* + $( + $request_packet_variant(ReqId, $request_packet_type), + )* + $( + $response_packet_variant(ReqId, $response_packet_type), + )* + } + impl SSHEncode for SftpPacket<'_> { fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { let t = u8::from(self.sftp_num()); @@ -383,7 +431,19 @@ macro_rules! sftpmessages { // SftpPacket::KexInit(p) => { // ... $( - SftpPacket::$SpecificPacketVariant(p) => { + SftpPacket::$init_packet_variant(p) => { + p.enc(s)? + } + )* + $( + SftpPacket::$request_packet_variant(id, p) => { + id.enc(s)?; + p.enc(s)? + } + )* + $( + SftpPacket::$response_packet_variant(id, p) => { + id.enc(s)?; p.enc(s)? } )* @@ -392,6 +452,8 @@ macro_rules! sftpmessages { } } + paste!{ + impl<'a: 'de, 'de> SSHDecode<'de> for SftpPacket<'a> where 'de: 'a // This implies that both lifetimes are equal @@ -404,9 +466,27 @@ macro_rules! sftpmessages { let decoded_packet = match packet_type { $( - SftpNum::$SSH_FXP_NAME => { - let inner_type = <$SpecificPacketType>::dec(s)?; - SftpPacket::$SpecificPacketVariant(inner_type) + SftpNum::[<$init_ssh_fxp_name:upper>] => { + + let inner_type = <$init_packet_type>::dec(s)?; + SftpPacket::$init_packet_variant(inner_type) + + }, + )* + $( + SftpNum::[<$request_ssh_fxp_name:upper>] => { + let req_id = ::dec(s)?; + let inner_type = <$request_packet_type>::dec(s)?; + SftpPacket::$request_packet_variant(req_id,inner_type) + + }, + )* + $( + SftpNum::[<$response_ssh_fxp_name:upper>] => { + let req_id = ::dec(s)?; + let inner_type = <$response_packet_type>::dec(s)?; + SftpPacket::$response_packet_variant(req_id,inner_type) + }, )* _ => return Err(WireError::UnknownPacket { number: packet_type_number }) @@ -414,6 +494,7 @@ macro_rules! sftpmessages { Ok(decoded_packet) } } + } // paste impl<'a> SftpPacket<'a> { /// Maps `SpecificPacketVariant` to `message_num` @@ -423,9 +504,21 @@ macro_rules! sftpmessages { // SftpPacket::Open(_) => { // .. $( - SftpPacket::$SpecificPacketVariant(_) => { + SftpPacket::$init_packet_variant(_) => { + + SftpNum::from($init_message_num as u8) + } + )* + $( + SftpPacket::$request_packet_variant(_,_) => { - SftpNum::from($message_num as u8) + SftpNum::from($request_message_num as u8) + } + )* + $( + SftpPacket::$response_packet_variant(_,_) => { + + SftpNum::from($response_message_num as u8) } )* } @@ -472,26 +565,47 @@ macro_rules! sftpmessages { Ok((id, Self::dec(s)?)) } - /// Decode a request. + + /// Decode a request. Includes Init /// - /// Used by a SFTP server. Does not include the length field. - pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> + /// Used by a SFTP server. Does not include the length field. If the request does not have id (Initialisation) + pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(Self)> where S: SSHSource<'de>, 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes 'de: 'a { - let num = SftpNum::from(u8::dec(s)?); - if !num.is_request() { - return Err(WireError::PacketWrong) - // return error::SSHProto.fail(); - // TODO: Not an error in the SSHProtocol rather the SFTP. - // Maybe is time to define an SftpError - } + // let sftp_packet = Self::dec(s)?; + // if (!sftp_packet.sftp_num().is_request() + // && !sftp_packet.sftp_num().is_init()) + // {return Err(WireError::PacketWrong)} - let id = ReqId(u32::dec(s)?); - Ok((id, Self::dec(s)?)) + // let maybe_id = if sftp_packet.sftp_num().is_init(){ + // None + // } else{ + + // Some(ReqId(u32::dec(s)?)) + // }; + + // let num = SftpNum::from(u8::dec(s)?); + + // if (!num.is_request() + // && !num.is_init()) + // {return Err(WireError::PacketWrong)} + + + // let maybe_id = if num.is_init(){ + // None + // } else{ + + // Some(ReqId(u32::dec(s)?)) + // }; + + let inner_sftp_packet = Self::dec(s)? ; + // let sftp_packet = Self::dec(s)?; + + Ok( inner_sftp_packet) } /// Encode a response. @@ -516,31 +630,56 @@ macro_rules! sftpmessages { } -$( -impl<'a> From<$SpecificPacketType> for SftpPacket<'a> { - fn from(s: $SpecificPacketType) -> SftpPacket<'a> { - SftpPacket::$SpecificPacketVariant(s) //find me - } -} -)* + $( + impl<'a> From<$init_packet_type> for SftpPacket<'a> { + fn from(s: $init_packet_type) -> SftpPacket<'a> { + SftpPacket::$init_packet_variant(s) //find me + } + } + )* + $( + /// **Warning**: No Sequence Id can be infered from a Packet Type + impl<'a> From<$request_packet_type> for SftpPacket<'a> { + fn from(s: $request_packet_type) -> SftpPacket<'a> { + warn!("Casting from {:?} to SftpPacket cannot set Request Id",$request_ssh_fxp_name); + SftpPacket::$request_packet_variant(ReqId(0), s) + } + } + )* + $( + /// **Warning**: No Sequence Id can be infered from a Packet Type + impl<'a> From<$response_packet_type> for SftpPacket<'a> { + fn from(s: $response_packet_type) -> SftpPacket<'a> { + warn!("Casting from {:?} to SftpPacket cannot set Request Id",$response_ssh_fxp_name); + SftpPacket::$response_packet_variant(ReqId(0), s) + } + } + )* + + }; // main macro + +} // sftpmessages macro -} } // macro +sftpmessages! [ -sftpmessages![ + init:{ + (1, Init, InitVersionClient, "ssh_fxp_init"), + (2, Version, InitVersionLowest, "ssh_fxp_version"), + }, -// Message number ranges are also used by Sftpnum::is_request and is_response. + request: { + (3, Open, Open<'a>, "ssh_fxp_open"), + (4, Close, Close<'a>, "ssh_fxp_close"), + (5, Read, Read<'a>, "ssh_fxp_read"), + (6, Write, Write<'a>, "ssh_fxp_write"), + (16, PathInfo, PathInfo<'a>, "ssh_fxp_realpath"), + }, -(1, Init, InitVersionClient, SSH_FXP_INIT), - (2, Version, InitVersionLowest, SSH_FXP_VERSION), - // Requests - (3, Open, Open<'a>, SSH_FXP_OPEN), - (4, Close, Close<'a>, SSH_FXP_CLOSE), - (5, Read, Read<'a>, SSH_FXP_READ), - (6, Write, Write<'a>, SSH_FXP_WRITE), + response: { + (101, Status, Status<'a>, "ssh_fxp_status"), + (102, Handle, Handle<'a>, "ssh_fxp_handle"), + (103, Data, Data<'a>, "ssh_fxp_data"), + (104, Name, Name<'a>, "ssh_fxp_name"), - // Responses - (101, Status, Status<'a>, SSH_FXP_STATUS), - (102, Handle, Handle<'a>, SSH_FXP_HANDLE), - (103, Data, Data<'a>, SSH_FXP_DATA), - (104, Name, Name<'a>, SSH_FXP_NAME), + }, ]; From 2fd52e4373e4574d004032d2ae51839d1922b837 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 4 Sep 2025 15:58:17 +1000 Subject: [PATCH 254/393] adding from str for Filename --- sftp/src/proto.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 4fb6f832..1ac36282 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -13,6 +13,12 @@ use log::{debug, error, info, log, trace, warn}; #[derive(Debug, SSHEncode, SSHDecode)] pub struct Filename<'a>(TextString<'a>); +impl<'a> From<&'a str> for Filename<'a> { + fn from(s: &'a str) -> Self { + Filename(TextString(s.as_bytes())) + } +} + #[derive(Debug, SSHEncode, SSHDecode)] pub struct FileHandle<'a>(pub BinString<'a>); From d4acbd1cac49e723ab8842e24736ffd3e75ad9f9 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 4 Sep 2025 16:03:37 +1000 Subject: [PATCH 255/393] changing ranges for is_init, is_request, is_response --- sftp/src/proto.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 1ac36282..fea11f9d 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -399,12 +399,13 @@ macro_rules! sftpmessages { fn is_request(&self) -> bool { // TODO SSH_FXP_EXTENDED - (2..=99).contains(&(u8::from(self.clone()))) + (3..=20).contains(&(u8::from(self.clone()))) } fn is_response(&self) -> bool { // TODO SSH_FXP_EXTENDED_REPLY - (100..=199).contains(&(u8::from(self.clone()))) + (100..=105).contains(&(u8::from(self.clone()))) + ||(2..=2).contains(&(u8::from(self.clone()))) } } From 60a331a37bb087b415587a384eaddeb00535dc76 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 4 Sep 2025 16:05:51 +1000 Subject: [PATCH 256/393] fixing decode_request --- sftp/src/proto.rs | 43 +++++++++++++------------------------------ 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index fea11f9d..9d784758 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -573,9 +573,11 @@ macro_rules! sftpmessages { } - /// Decode a request. Includes Init + /// Decode a request. Includes Initialization packets /// - /// Used by a SFTP server. Does not include the length field. If the request does not have id (Initialisation) + /// Used by a SFTP server. Does not include the length field. + /// + /// It will fail if the received packet is a response pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(Self)> where S: SSHSource<'de>, @@ -583,41 +585,22 @@ macro_rules! sftpmessages { 'de: 'a { - // let sftp_packet = Self::dec(s)?; - // if (!sftp_packet.sftp_num().is_request() - // && !sftp_packet.sftp_num().is_init()) - // {return Err(WireError::PacketWrong)} - - // let maybe_id = if sftp_packet.sftp_num().is_init(){ - // None - // } else{ - - // Some(ReqId(u32::dec(s)?)) - // }; - - // let num = SftpNum::from(u8::dec(s)?); - - // if (!num.is_request() - // && !num.is_init()) - // {return Err(WireError::PacketWrong)} + let sftp_packet = Self::dec(s)? ; + if (!sftp_packet.sftp_num().is_request() + && !sftp_packet.sftp_num().is_init()) + { + return Err(WireError::PacketWrong) + } - // let maybe_id = if num.is_init(){ - // None - // } else{ - - // Some(ReqId(u32::dec(s)?)) - // }; - - let inner_sftp_packet = Self::dec(s)? ; - // let sftp_packet = Self::dec(s)?; - - Ok( inner_sftp_packet) + Ok( sftp_packet) } /// Encode a response. /// /// Used by a SFTP server. Does not include the length field. + /// + /// Fails if the encoded SFTP Packet is not a response pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { if !self.sftp_num().is_response() { From 539031391f20c34c0a836b4dea77f44edbd07529 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 4 Sep 2025 16:10:47 +1000 Subject: [PATCH 257/393] Fixing the exposed items in SFTP library. More changes will come --- sftp/src/lib.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index ae003bca..1db3d32c 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -2,11 +2,14 @@ mod proto; mod sftpserver; pub use sftpserver::DirReply; +pub use sftpserver::ItemHandle; pub use sftpserver::ReadReply; -pub use sftpserver::Result; +pub use sftpserver::SftpHandler; +pub use sftpserver::SftpResult; pub use sftpserver::SftpServer; pub use proto::Attrs; -pub use proto::SFTP_VERSION; -pub use proto::SftpNum; -pub use proto::SftpPacket; +pub use proto::Filename; +pub use proto::Name; +pub use proto::NameEntry; +pub use proto::PathInfo; From 463b76bf2f8766bdb5b82e8e22cef14f9d366ae1 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 4 Sep 2025 16:12:07 +1000 Subject: [PATCH 258/393] added sunset-sftp to sftp demo --- demo/sftp/std/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/sftp/std/Cargo.toml b/demo/sftp/std/Cargo.toml index da11494c..0c2a8205 100644 --- a/demo/sftp/std/Cargo.toml +++ b/demo/sftp/std/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" sunset = { workspace = true, features = ["rsa", "std"] } sunset-async.workspace = true sunset-demo-common.workspace = true +sunset-sftp = { version = "0.1.0", path = "../../../sftp" } # 131072 was determined empirically embassy-executor = { version = "0.7", features = [ From e5c02ba1fcc3655dadfb369b7a22253c05b1d045 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 4 Sep 2025 16:13:08 +1000 Subject: [PATCH 259/393] adding log dep to sunset-sftp --- Cargo.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 5def08c7..e709aa23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2871,6 +2871,7 @@ dependencies = [ "sunset", "sunset-async", "sunset-demo-common", + "sunset-sftp", ] [[package]] @@ -2915,6 +2916,7 @@ dependencies = [ name = "sunset-sftp" version = "0.1.0" dependencies = [ + "log", "num_enum 0.7.4", "paste", "sunset", From 8cab055a5e3e4d1ff05cfa93ca03c1abffef11c7 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 5 Sep 2025 09:58:52 +1000 Subject: [PATCH 260/393] WIP: Adding demosftpserver.rs This will be the local implementation to serve the requests from the client. For now just structure and a mock realpath --- demo/sftp/std/src/demosftpserver.rs | 72 +++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 demo/sftp/std/src/demosftpserver.rs diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs new file mode 100644 index 00000000..44318953 --- /dev/null +++ b/demo/sftp/std/src/demosftpserver.rs @@ -0,0 +1,72 @@ +use std::str::FromStr; + +use sunset::TextString; +use sunset_sftp::{ + Attrs, DirReply, Filename, ItemHandle, Name, NameEntry, ReadReply, SftpResult, + SftpServer, +}; + +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; + +pub struct DemoSftpServer {} + +impl SftpServer for DemoSftpServer { + type Handle = ItemHandle; + + async fn open( + filename: &str, + flags: u32, + attrs: &Attrs, + ) -> SftpResult { + todo!() + } + + async fn close(handle: &Self::Handle) -> SftpResult<()> { + todo!() + } + + async fn read( + handle: &Self::Handle, + offset: u64, + reply: &mut ReadReply<'_, '_>, + ) -> SftpResult<()> { + todo!() + } + + async fn write( + handle: &Self::Handle, + offset: u64, + buf: &[u8], + ) -> SftpResult<()> { + todo!() + } + + async fn opendir(dir: &str) -> SftpResult { + todo!() + } + + async fn readdir( + handle: &Self::Handle, + reply: &mut DirReply<'_, '_>, + ) -> SftpResult<()> { + todo!() + } + + async fn realpath(dir: &str) -> SftpResult> { + debug!("finding path for: {:?}", dir); + Ok(Name(vec![NameEntry { + filename: Filename::from("/root/just/kidding"), + _longname: Filename::from(""), + attrs: Attrs { + size: None, + uid: None, + gid: None, + permissions: None, + atime: None, + mtime: None, + ext_count: None, + }, + }])) + } +} From 02e4f3ef5bf73421b00b22efdd71f99e1da496d2 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 5 Sep 2025 10:00:01 +1000 Subject: [PATCH 261/393] adding the module demosftpserver to the demo. Reordering uses --- demo/sftp/std/src/main.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 2e43fda2..bf1b5536 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -1,6 +1,14 @@ +use sunset::*; +use sunset_async::{ProgressHolder, SSHServer, SunsetMutex, SunsetRawMutex}; +use sunset_sftp::SftpHandler; + +pub(crate) use sunset_demo_common as demo_common; + +use demo_common::{DemoCommon, DemoServer, SSHConfig}; + +use crate::demosftpserver::DemoSftpServer; + use embedded_io_async::{Read, Write}; -#[allow(unused_imports)] -use log::{debug, error, info, log, trace, warn}; use embassy_executor::Spawner; use embassy_net::{Stack, StackResources, StaticConfigV4}; @@ -12,12 +20,10 @@ use embassy_futures::select::select; use embassy_net_tuntap::TunTapDevice; use embassy_sync::channel::Channel; -use sunset::*; -use sunset_async::{ProgressHolder, SSHServer, SunsetMutex, SunsetRawMutex}; - -pub(crate) use sunset_demo_common as demo_common; +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; -use demo_common::{DemoCommon, DemoServer, SSHConfig}; +mod demosftpserver; const NUM_LISTENERS: usize = 4; // +1 for dhcp From ec5420c39794fb72d527b041747cac5c039ce97a Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 5 Sep 2025 10:00:57 +1000 Subject: [PATCH 262/393] Reordering uses to clarify internal and external dependencies --- sftp/src/proto.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 9d784758..6fff1bda 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,14 +1,13 @@ -use num_enum::FromPrimitive; -use paste::paste; use sunset::sshwire::{ BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, WireResult, }; - use sunset_sshwire_derive::{SSHDecode, SSHEncode}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; +use num_enum::FromPrimitive; +use paste::paste; // TODO is utf8 enough, or does this need to be an opaque binstring? #[derive(Debug, SSHEncode, SSHDecode)] pub struct Filename<'a>(TextString<'a>); @@ -593,7 +592,7 @@ macro_rules! sftpmessages { return Err(WireError::PacketWrong) } - Ok( sftp_packet) + Ok(sftp_packet) } /// Encode a response. From 345a92354c881f239ad9507ace5730650e8ebaf0 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 5 Sep 2025 10:01:52 +1000 Subject: [PATCH 263/393] tyding up dependencies --- sftp/src/sftpserver.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index bbee31de..b250ce6a 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,7 +1,8 @@ -use crate::proto::{Attrs, StatusCode}; +use crate::proto::{Attrs, Name, StatusCode}; + use core::marker::PhantomData; -pub type Result = core::result::Result; +pub type SftpResult = core::result::Result; /// All trait functions are optional in the SFTP protocol. /// Some less core operations have a Provided implementation returning From b9dc9e24b723241c3fd001155b7e1381e2137966 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 5 Sep 2025 10:05:50 +1000 Subject: [PATCH 264/393] adding trait associated function realpath and struct ItemHandle Still deciding on how to use ItemHandle --- sftp/src/sftpserver.rs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index b250ce6a..f69342c3 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -12,23 +12,30 @@ pub trait SftpServer { type Handle; // TODO flags struct - async fn open(filename: &str, flags: u32, attrs: &Attrs) - -> Result; + async fn open( + filename: &str, + flags: u32, + attrs: &Attrs, + ) -> SftpResult; /// Close either a file or directory handle - async fn close(handle: &Self::Handle) -> Result<()>; + async fn close(handle: &Self::Handle) -> SftpResult<()>; async fn read( handle: &Self::Handle, offset: u64, reply: &mut ReadReply, - ) -> Result<()>; + ) -> SftpResult<()>; - async fn write(handle: &Self::Handle, offset: u64, buf: &[u8]) -> Result<()>; + async fn write(handle: &Self::Handle, offset: u64, buf: &[u8]) + -> SftpResult<()>; - async fn opendir(dir: &str) -> Result; + async fn opendir(dir: &str) -> SftpResult; - async fn readdir(handle: &Self::Handle, reply: &mut DirReply) -> Result<()>; + async fn readdir(handle: &Self::Handle, reply: &mut DirReply) -> SftpResult<()>; + + /// Provides the real path of the directory specified + async fn realpath(dir: &str) -> SftpResult>; } pub struct ReadReply<'g, 'a> { @@ -52,3 +59,8 @@ pub struct ChanOut<'g, 'a> { _phantom_g: PhantomData<&'g ()>, _phantom_a: PhantomData<&'a ()>, } + +#[derive(Debug)] +pub struct ItemHandle { + client_opaque_handle: String, +} From c849b25a26531106d2dc2154c73c4fe435f62e65 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 5 Sep 2025 10:17:42 +1000 Subject: [PATCH 265/393] WIP Added sftphandle.rs This is the "traffic coordinator" for SFTP. - Handles incoming packets deserialisation - Handle the initialisation - Calls SftpServer trait to handle client requests - Handles incoming packets serialisation The current implementation tries limiting its dependencies to the minimum. Therefore it does not handle the SSH Channels itself but only buffers. This also allow the user to make decisions on the buffers. the sftp demo has been updated to use this, delegating the sftp details to the sftphandle and demosftpserver --- demo/sftp/std/src/main.rs | 60 ++++++------ sftp/src/lib.rs | 4 +- sftp/src/sftphandle.rs | 198 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+), 31 deletions(-) create mode 100644 sftp/src/sftphandle.rs diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index bf1b5536..85b7021d 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -88,7 +88,7 @@ impl DemoServer for StdDemo { async fn run(&self, serv: &SSHServer<'_>, mut common: DemoCommon) -> Result<()> { let chan_pipe = Channel::::new(); - let prog_loop = async { + let prog_loop_inner = async { loop { let mut ph = ProgressHolder::new(); let ev = serv.progress(&mut ph).await?; @@ -130,43 +130,41 @@ impl DemoServer for StdDemo { }; let prog_loop = async { - if let Err(e) = prog_loop.await { - warn!("Exited: {e:?}"); + info!("prog_loop started"); + if let Err(e) = prog_loop_inner.await { + warn!("Prog Loop Exited: {e:?}"); + return Err(e); } + Ok(()) }; let sftp_loop = async { let ch = chan_pipe.receive().await; - info!("This is the sftp loop"); - // After this you are likely to receive an SSH_FXP_INIT + info!("SFTP loop has received a channel handle"); + let mut stdio = serv.stdio(ch).await?; + let mut buffer_in = [0u8; 1000]; + let mut buffer_out = [0u8; 1000]; + + let mut sftp_handler = + SftpHandler::::new(&buffer_in, &mut buffer_out); loop { - let mut b = [0u8; 200]; - let lr = stdio.read(&mut b).await?; - if lr == 0 { - break; - } - let b = &mut b[..lr]; - info!("received '{:?}'", b); - - // First packet received! [0, 0, 0, 5, 1, 0, 0, 0, 3], meaning: - // Len (u32): [0, 0, 0, 5] 5 bytes - // Type (u8): [1] SSH_FXP_INIT - // Version (u32): [0, 0, 0, 3]] version 3 - // So we need to answer 5 SSH_FXP_VERSION 3 - // [0, 0, 0, 5, 2, 0, 0, 0, 3] - let hardcoded_version_r: Vec = vec![0, 0, 0, 5, 2, 0, 0, 0, 3]; - info!("sending '{:?}'", hardcoded_version_r); - - stdio.write(hardcoded_version_r.as_slice()).await?; - } + let lr = stdio.read(&mut buffer_in).await?; + debug!("SFTP <---- received: {:?}", &buffer_in[0..lr]); + + let lw = + sftp_handler.process(&buffer_in[0..lr], &mut buffer_out).await?; + stdio.write(&mut buffer_out[0..lw]).await?; + debug!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); + } Ok::<_, Error>(()) }; - select(prog_loop, sftp_loop).await; - todo!() + let selected = select(prog_loop, sftp_loop).await; + error!("Selected finished: {:?}", selected); + todo!("Loop terminated: {:?}", selected) } } @@ -183,14 +181,16 @@ async fn listen( #[embassy_executor::main] async fn main(spawner: Spawner) { env_logger::builder() - .filter_level(log::LevelFilter::Trace) - // .filter_module("sunset::runner", log::LevelFilter::Info) + .filter_level(log::LevelFilter::Debug) + .filter_module("sunset::runner", log::LevelFilter::Info) .filter_module("sunset::traffic", log::LevelFilter::Info) .filter_module("sunset::encrypt", log::LevelFilter::Info) - // .filter_module("sunset::conn", log::LevelFilter::Info) - // .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) + .filter_module("sunset::conn", log::LevelFilter::Info) + .filter_module("sunset::kex", log::LevelFilter::Info) + .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) .filter_module("async_io", log::LevelFilter::Info) .filter_module("polling", log::LevelFilter::Info) + .filter_module("embassy_net", log::LevelFilter::Info) .format_timestamp_nanos() .init(); diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 1db3d32c..625fe3df 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,13 +1,15 @@ mod proto; +mod sftphandle; mod sftpserver; pub use sftpserver::DirReply; pub use sftpserver::ItemHandle; pub use sftpserver::ReadReply; -pub use sftpserver::SftpHandler; pub use sftpserver::SftpResult; pub use sftpserver::SftpServer; +pub use sftphandle::SftpHandler; + pub use proto::Attrs; pub use proto::Filename; pub use proto::Name; diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs new file mode 100644 index 00000000..a83086bd --- /dev/null +++ b/sftp/src/sftphandle.rs @@ -0,0 +1,198 @@ +use crate::proto::{ + InitVersionLowest, ReqId, SFTP_VERSION, SftpPacket, Status, StatusCode, +}; +use crate::sftpserver::{ItemHandle, SftpServer}; + +use sunset::sshwire::{SSHDecode, SSHSink, SSHSource, WireError, WireResult}; + +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; +use std::marker::PhantomData; + +#[derive(Default, Debug)] +pub struct SftpSource<'de> { + pub buffer: &'de [u8], + pub index: usize, +} + +impl<'de> SSHSource<'de> for SftpSource<'de> { + fn take(&mut self, len: usize) -> sunset::sshwire::WireResult<&'de [u8]> { + if len + self.index > self.buffer.len() { + return Err(WireError::NoRoom); + } + let original_index = self.index; + let slice = &self.buffer[self.index..self.index + len]; + self.index += len; + trace!( + "slice returned: {:?}. original index {:?}, new index: {:?}", + slice, original_index, self.index + ); + Ok(slice) + } + + fn remaining(&self) -> usize { + self.buffer.len() - self.index + } + + fn ctx(&mut self) -> &mut sunset::packets::ParseContext { + todo!( + "I don't know what to do with the ctx, since sftp does not have context" + ); + } +} + +// // This implementation is an extension of the SSHSource interface. +// impl<'de> SftpSource<'de> { +// /// Rewinds the index back to the initial byte +// /// +// /// In case of an error deserialising the SSHSource it allows reprocesing the buffer from start +// pub fn rewind(&mut self) -> () { +// self.index = 0; +// } +// } + +#[derive(Default)] +pub struct SftpSink<'g> { + pub buffer: &'g mut [u8], + index: usize, +} + +impl<'g> SftpSink<'g> { + const LENG_FIELD_LEN: usize = 4; // TODO: Move it to a better location + + pub fn new(s: &'g mut [u8]) -> Self { + SftpSink { buffer: s, index: 0 } + // SftpSink { buffer: s, index: SftpSink::LENG_FIELD_LEN } + } + + /// Finalise the buffer by prepending the payload size and returning + /// + /// Returns the final index in the buffer as a reference for the space used + pub fn finalise(&mut self) -> usize { + if self.index <= SftpSink::LENG_FIELD_LEN { + warn!("SftpSink trying to terminate it before pushing data"); + return 0; + } // size is 0 + let used_size = (self.index - 4) as u32; + + used_size + .to_be_bytes() + .iter() + .enumerate() + .for_each(|(i, v)| self.buffer[i] = *v); + + self.index + } +} + +impl<'g> SSHSink for SftpSink<'g> { + fn push(&mut self, v: &[u8]) -> sunset::sshwire::WireResult<()> { + if v.len() + self.index > self.buffer.len() { + return Err(WireError::NoRoom); + } + v.iter().for_each(|val| { + self.buffer[self.index] = *val; + self.index += 1; + }); + {} + Ok(()) + } +} + +#[derive(Debug)] +pub struct SftpHandler +where + T: SftpServer, +{ + server_type: PhantomData, + handle_list: Vec, + initialized: bool, +} + +impl SftpHandler +where + T: SftpServer, +{ + pub fn new(buffer_in: &[u8], buffer_out: &mut [u8]) -> Self { + SftpHandler { + server_type: PhantomData, + handle_list: vec![], + initialized: false, + } + } + + /// Decodes the buffer_in request, process the request delegating operations to an Struct implementing SftpServer, + /// serialises an answer in buffer_out and return the length usedd in buffer_out + pub async fn process( + &mut self, + buffer_in: &[u8], + buffer_out: &mut [u8], + ) -> WireResult { + if buffer_in.len() < 4 { + return Err(WireError::PacketWrong); + } + + let mut source = SftpSource { buffer: buffer_in, index: 0 }; + trace!("Source content: {:?}", source); + + let packet_length = u32::dec(&mut source)?; + trace!("Packet field lenght content: {}", packet_length); + + let mut sink = SftpSink::new(buffer_out); + + // TODO: Handle gracesfully unknow packets + let request = match SftpPacket::decode_request(&mut source) { + Ok(request) => { + info!("received request: {:?}", request); + request + } + Err(e) => { + warn!("Could not decode the request: {:?}", e); + return Err(e); + } + }; + + if !self.initialized && !matches!(request, SftpPacket::Init(_)) { + return Err(WireError::SSHProto); // TODO: Start using the SFTP Errors + } + + match request { + SftpPacket::Init(_) => { + // TODO: Do a real check, provide the lowest version or return an error if the client cannot handle the server SFTP_VERSION + let version = + SftpPacket::Version(InitVersionLowest { version: SFTP_VERSION }); + + info!("Sending '{:?}'", version); + + version.encode_response(ReqId(0), &mut sink)?; + + self.initialized = true; + } + SftpPacket::PathInfo(req_id, path_info) => { + let a_name = + T::realpath(path_info.path.as_str().expect( + "Could not deref and the errors are not harmonised", + )) + .await + .expect("Could not deref and the errors are not harmonised"); + + let response = SftpPacket::Name(req_id, a_name); + + response.encode_response(req_id, &mut sink)?; + } + _ => { + let response = SftpPacket::Status( + ReqId(0), + Status { + code: StatusCode::SSH_FX_OP_UNSUPPORTED, + message: "Not implemented".into(), + lang: "EN".into(), + }, + ); + response.encode_response(ReqId(0), &mut sink)?; + } + }; + + Ok(sink.finalise()) + } +} From ec075a0658cc7b438b0e5b9be32943105a03aaa3 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Sat, 6 Sep 2025 16:41:00 +1000 Subject: [PATCH 266/393] Adding as_str for Filename Just gets the inner BinString as_str --- sftp/src/proto.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 6fff1bda..a415b0a8 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -18,6 +18,12 @@ impl<'a> From<&'a str> for Filename<'a> { } } +impl<'a> Filename<'a> { + pub fn as_str(&self) -> Result<&'a str, WireError> { + core::str::from_utf8(self.0.0).map_err(|_| WireError::BadString) + } +} + #[derive(Debug, SSHEncode, SSHDecode)] pub struct FileHandle<'a>(pub BinString<'a>); From 47ee3f74b05f9ce9986c4bfc1f2b2e3de5bb27d6 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Sat, 6 Sep 2025 16:44:46 +1000 Subject: [PATCH 267/393] Handling the conditions where the SFTP protocol fails, setting things up so it can be handle and will wait for a new channel on the pipe from the program loop --- demo/sftp/std/src/main.rs | 53 +++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 85b7021d..e59e8c92 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -139,25 +139,22 @@ impl DemoServer for StdDemo { }; let sftp_loop = async { - let ch = chan_pipe.receive().await; - info!("SFTP loop has received a channel handle"); - - let mut stdio = serv.stdio(ch).await?; - let mut buffer_in = [0u8; 1000]; - let mut buffer_out = [0u8; 1000]; - - let mut sftp_handler = - SftpHandler::::new(&buffer_in, &mut buffer_out); - loop { - let lr = stdio.read(&mut buffer_in).await?; - debug!("SFTP <---- received: {:?}", &buffer_in[0..lr]); + let ch = chan_pipe.receive().await; - let lw = - sftp_handler.process(&buffer_in[0..lr], &mut buffer_out).await?; + info!("SFTP loop has received a channel handle {:?}", ch.num()); - stdio.write(&mut buffer_out[0..lw]).await?; - debug!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); + let buffer_in = [0u8; 1000]; + let buffer_out = [0u8; 1000]; + + match sftp_server_loop(serv, buffer_in, buffer_out, ch).await { + Ok(_) => { + warn!("sftp server loop finished gracefully"); + } + Err(e) => { + warn!("sftp server loop finished with an error: {}", e) + } + }; } Ok::<_, Error>(()) }; @@ -168,6 +165,30 @@ impl DemoServer for StdDemo { } } +async fn sftp_server_loop( + serv: &SSHServer<'_>, + mut buffer_in: [u8; 1000], + mut buffer_out: [u8; 1000], + ch: ChanHandle, +) -> Result<(), Error> { + let mut stdio = serv.stdio(ch).await?; + let mut sftp_handler = + SftpHandler::::new(&buffer_in, &mut buffer_out); + Ok(loop { + let lr = stdio.read(&mut buffer_in).await?; + debug!("SFTP <---- received: {:?}", &buffer_in[0..lr]); + if lr == 0 { + debug!("client disconnected"); + return Ok(()); + } + + let lw = sftp_handler.process(&buffer_in[0..lr], &mut buffer_out).await?; + + stdio.write(&mut buffer_out[0..lw]).await?; + debug!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); + }) +} + // TODO: pool_size should be NUM_LISTENERS but needs a literal #[embassy_executor::task(pool_size = 4)] async fn listen( From 0004394df385af2bb487315423bcbeaee3372e6b Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Sat, 6 Sep 2025 16:48:50 +1000 Subject: [PATCH 268/393] removing std uses --- sftp/src/sftphandle.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index a83086bd..11358a02 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -5,9 +5,10 @@ use crate::sftpserver::{ItemHandle, SftpServer}; use sunset::sshwire::{SSHDecode, SSHSink, SSHSource, WireError, WireResult}; +use core::marker::PhantomData; +use core::u32; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -use std::marker::PhantomData; #[derive(Default, Debug)] pub struct SftpSource<'de> { From 068cb195b405a377b926ec90fccb55e2e54af41a Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 05:45:13 +1000 Subject: [PATCH 269/393] Reformed sftpserver - flags are no longer required - removed ItemHandle - Added Into and TryFrom File Handle for SftpServer trait - Added lifetime 'a to Handle and the trait associated function taking or returning the type minor changes in proto: added clone, copy and compare implements to FileHandle and TextString --- sftp/src/proto.rs | 4 ++-- sftp/src/sftpserver.rs | 42 ++++++++++++++++++++++-------------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index a415b0a8..c9ea2192 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -24,7 +24,7 @@ impl<'a> Filename<'a> { } } -#[derive(Debug, SSHEncode, SSHDecode)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, SSHEncode, SSHDecode)] pub struct FileHandle<'a>(pub BinString<'a>); /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 @@ -86,7 +86,7 @@ pub struct Status<'a> { pub lang: TextString<'a>, } -#[derive(Debug, SSHEncode, SSHDecode)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, SSHEncode, SSHDecode)] pub struct Handle<'a> { pub handle: FileHandle<'a>, } diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index f69342c3..72b1593c 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,41 +1,48 @@ -use crate::proto::{Attrs, Name, StatusCode}; +use crate::proto::{Attrs, FileHandle, Name, StatusCode}; +use core::fmt::Debug; use core::marker::PhantomData; -pub type SftpResult = core::result::Result; +pub type SftpOpResult = core::result::Result; /// All trait functions are optional in the SFTP protocol. /// Some less core operations have a Provided implementation returning /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. pub trait SftpServer { - type Handle; + // type Handle: Into + TryFrom + Debug; + type Handle<'a>: Into> + TryFrom> + Debug + Copy; // TODO flags struct - async fn open( + async fn open<'a>( filename: &str, - flags: u32, attrs: &Attrs, - ) -> SftpResult; + ) -> SftpOpResult>; /// Close either a file or directory handle - async fn close(handle: &Self::Handle) -> SftpResult<()>; + async fn close<'a>(handle: &Self::Handle<'a>) -> SftpOpResult<()>; - async fn read( - handle: &Self::Handle, + async fn read<'a>( + handle: &Self::Handle<'a>, offset: u64, reply: &mut ReadReply, - ) -> SftpResult<()>; + ) -> SftpOpResult<()>; - async fn write(handle: &Self::Handle, offset: u64, buf: &[u8]) - -> SftpResult<()>; + async fn write<'a>( + handle: &Self::Handle<'a>, + offset: u64, + buf: &[u8], + ) -> SftpOpResult<()>; - async fn opendir(dir: &str) -> SftpResult; + async fn opendir<'a>(dir: &str) -> SftpOpResult>; - async fn readdir(handle: &Self::Handle, reply: &mut DirReply) -> SftpResult<()>; + async fn readdir<'a>( + handle: &Self::Handle<'a>, + reply: &mut DirReply, + ) -> SftpOpResult<()>; /// Provides the real path of the directory specified - async fn realpath(dir: &str) -> SftpResult>; + async fn realpath(dir: &str) -> SftpOpResult>; } pub struct ReadReply<'g, 'a> { @@ -59,8 +66,3 @@ pub struct ChanOut<'g, 'a> { _phantom_g: PhantomData<&'g ()>, _phantom_a: PhantomData<&'a ()>, } - -#[derive(Debug)] -pub struct ItemHandle { - client_opaque_handle: String, -} From 1309a2c62ec81acda1ba3b2ba60f55d08566f701 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 05:49:07 +1000 Subject: [PATCH 270/393] Bug fix in SftpPacket.encode_request enconde+request was encoding sftp number and the request id, which is incoded in self.enc(c) call. Fixed and removed req_id from parameter list --- sftp/src/proto.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index c9ea2192..b098fb86 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -606,7 +606,7 @@ macro_rules! sftpmessages { /// Used by a SFTP server. Does not include the length field. /// /// Fails if the encoded SFTP Packet is not a response - pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { + pub fn encode_response(&self, s: &mut dyn SSHSink) -> WireResult<()> { if !self.sftp_num().is_response() { return Err(WireError::PacketWrong) @@ -615,11 +615,6 @@ macro_rules! sftpmessages { // therefore a bug, bug Error::bug() is not compatible with WireResult } - // packet type - self.sftp_num().enc(s)?; - // request ID - id.0.enc(s)?; - // contents self.enc(s) } From c73a81e9e204630cd550a767b463109c1de26aea Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 05:56:32 +1000 Subject: [PATCH 271/393] Working on sftphandle - handling unsupported packet - fixing calls to SftpPacket.encode_request - adding 'a lifetime to SftpHandler to handle a Vec of FileHandle<'a> --- sftp/src/sftphandle.rs | 105 +++++++++++++++++++++++++++++------------ 1 file changed, 76 insertions(+), 29 deletions(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 11358a02..4aea15ae 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,7 +1,8 @@ use crate::proto::{ - InitVersionLowest, ReqId, SFTP_VERSION, SftpPacket, Status, StatusCode, + self, FileHandle, InitVersionLowest, ReqId, SFTP_VERSION, SftpPacket, Status, + StatusCode, }; -use crate::sftpserver::{ItemHandle, SftpServer}; +use crate::sftpserver::SftpServer; use sunset::sshwire::{SSHDecode, SSHSink, SSHSource, WireError, WireResult}; @@ -62,8 +63,7 @@ impl<'g> SftpSink<'g> { const LENG_FIELD_LEN: usize = 4; // TODO: Move it to a better location pub fn new(s: &'g mut [u8]) -> Self { - SftpSink { buffer: s, index: 0 } - // SftpSink { buffer: s, index: SftpSink::LENG_FIELD_LEN } + SftpSink { buffer: s, index: Self::LENG_FIELD_LEN } } /// Finalise the buffer by prepending the payload size and returning @@ -91,26 +91,27 @@ impl<'g> SSHSink for SftpSink<'g> { if v.len() + self.index > self.buffer.len() { return Err(WireError::NoRoom); } + trace!("Sink index: {:}", self.index); v.iter().for_each(|val| { self.buffer[self.index] = *val; self.index += 1; }); - {} + trace!("Sink new index: {:}", self.index); Ok(()) } } -#[derive(Debug)] -pub struct SftpHandler +#[derive(Debug, Clone)] +pub struct SftpHandler<'a, T> where T: SftpServer, { server_type: PhantomData, - handle_list: Vec, + handle_list: Vec>, initialized: bool, } -impl SftpHandler +impl<'a, T> SftpHandler<'a, T> where T: SftpServer, { @@ -142,21 +143,37 @@ where let mut sink = SftpSink::new(buffer_out); // TODO: Handle gracesfully unknow packets - let request = match SftpPacket::decode_request(&mut source) { + match SftpPacket::decode_request(&mut source) { Ok(request) => { info!("received request: {:?}", request); - request - } - Err(e) => { - warn!("Could not decode the request: {:?}", e); - return Err(e); + self.process_known_request(&mut sink, request).await?; } + Err(e) => match e { + WireError::UnknownPacket { number } => { + warn!("Unsuported Packet Number {:?} {:?}", number, e); + push_unsuported(ReqId(u32::MAX), &mut sink)?; + } + _ => { + error!("Could not decode the request: {:?}", e); + return Err(e); + } + }, }; + Ok(sink.finalise()) + } + + async fn process_known_request( + &mut self, + sink: &mut SftpSink<'_>, + request: SftpPacket<'_>, + ) -> Result<(), WireError> + where + T: SftpServer, + { if !self.initialized && !matches!(request, SftpPacket::Init(_)) { return Err(WireError::SSHProto); // TODO: Start using the SFTP Errors } - match request { SftpPacket::Init(_) => { // TODO: Do a real check, provide the lowest version or return an error if the client cannot handle the server SFTP_VERSION @@ -165,7 +182,7 @@ where info!("Sending '{:?}'", version); - version.encode_response(ReqId(0), &mut sink)?; + version.encode_response(sink)?; self.initialized = true; } @@ -179,21 +196,51 @@ where let response = SftpPacket::Name(req_id, a_name); - response.encode_response(req_id, &mut sink)?; + response.encode_response(sink)?; + } + SftpPacket::Open(req_id, open) => { + match T::open(open.filename.as_str()?, &open.attrs).await { + Ok(handle) => { + self.handle_list.push(handle.into()); + let response = SftpPacket::Handle( + req_id, + proto::Handle { handle: handle.into() }, + ); + response.encode_response(sink)?; + info!("Sending '{:?}'", response); + } + Err(status_code) => { + let response = SftpPacket::Status( + req_id, + Status { + code: status_code, + message: "".into(), + lang: "EN".into(), + }, + ); + response.encode_request(req_id, sink)?; + info!("Sending '{:?}'", response); + } + }; + // push_unsuported(req_id, sink)?; } _ => { - let response = SftpPacket::Status( - ReqId(0), - Status { - code: StatusCode::SSH_FX_OP_UNSUPPORTED, - message: "Not implemented".into(), - lang: "EN".into(), - }, - ); - response.encode_response(ReqId(0), &mut sink)?; + push_unsuported(ReqId(0), sink)?; } }; - - Ok(sink.finalise()) + Ok(()) } } + +fn push_unsuported(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireError> { + let response = SftpPacket::Status( + req_id, + Status { + code: StatusCode::SSH_FX_OP_UNSUPPORTED, + message: "Not implemented".into(), + lang: "EN".into(), + }, + ); + response.encode_response(sink)?; + Ok(()) +} From a7752743e8437d0d362a59ecf15d58d135231176 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 05:56:41 +1000 Subject: [PATCH 272/393] sftp.rs - removed ItemHandle; + Adding StatusCode + FileHandle --- sftp/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 625fe3df..1501b72d 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,17 +1,19 @@ mod proto; +mod sftperror; mod sftphandle; mod sftpserver; pub use sftpserver::DirReply; -pub use sftpserver::ItemHandle; pub use sftpserver::ReadReply; -pub use sftpserver::SftpResult; +pub use sftpserver::SftpOpResult; pub use sftpserver::SftpServer; pub use sftphandle::SftpHandler; pub use proto::Attrs; +pub use proto::FileHandle; pub use proto::Filename; pub use proto::Name; pub use proto::NameEntry; pub use proto::PathInfo; +pub use proto::StatusCode; From a8fb7de6f823157c532819e4d7a6faa2e60bfbe8 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 05:57:10 +1000 Subject: [PATCH 273/393] minor fix in decode_request return value --- sftp/src/proto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index b098fb86..65125ca6 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -583,7 +583,7 @@ macro_rules! sftpmessages { /// Used by a SFTP server. Does not include the length field. /// /// It will fail if the received packet is a response - pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(Self)> + pub fn decode_request<'de, S>(s: &mut S) -> WireResult where S: SSHSource<'de>, 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes From 3299ecd6c3b31e3187c5310256aaac29c327514b Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 06:07:59 +1000 Subject: [PATCH 274/393] changes in demosftpserver very unstable at this point --- demo/sftp/std/src/demosftpserver.rs | 42 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 44318953..19fd22dd 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -1,9 +1,6 @@ -use std::str::FromStr; - -use sunset::TextString; use sunset_sftp::{ - Attrs, DirReply, Filename, ItemHandle, Name, NameEntry, ReadReply, SftpResult, - SftpServer, + Attrs, DirReply, FileHandle, Filename, Name, NameEntry, ReadReply, SftpOpResult, + SftpServer, StatusCode, }; #[allow(unused_imports)] @@ -12,48 +9,49 @@ use log::{debug, error, info, log, trace, warn}; pub struct DemoSftpServer {} impl SftpServer for DemoSftpServer { - type Handle = ItemHandle; + type Handle<'a> = FileHandle<'a>; - async fn open( + async fn open<'a>( filename: &str, - flags: u32, + // flags: u32, attrs: &Attrs, - ) -> SftpResult { - todo!() + ) -> SftpOpResult> { + warn!("Wont allow open!"); + Err(StatusCode::SSH_FX_PERMISSION_DENIED) } - async fn close(handle: &Self::Handle) -> SftpResult<()> { + async fn close<'a>(handle: &Self::Handle<'a>) -> SftpOpResult<()> { todo!() } - async fn read( - handle: &Self::Handle, + async fn read<'a>( + handle: &Self::Handle<'a>, offset: u64, reply: &mut ReadReply<'_, '_>, - ) -> SftpResult<()> { + ) -> SftpOpResult<()> { todo!() } - async fn write( - handle: &Self::Handle, + async fn write<'a>( + handle: &Self::Handle<'a>, offset: u64, buf: &[u8], - ) -> SftpResult<()> { + ) -> SftpOpResult<()> { todo!() } - async fn opendir(dir: &str) -> SftpResult { + async fn opendir<'a>(dir: &str) -> SftpOpResult> { todo!() } - async fn readdir( - handle: &Self::Handle, + async fn readdir<'a>( + handle: &Self::Handle<'a>, reply: &mut DirReply<'_, '_>, - ) -> SftpResult<()> { + ) -> SftpOpResult<()> { todo!() } - async fn realpath(dir: &str) -> SftpResult> { + async fn realpath(dir: &str) -> SftpOpResult> { debug!("finding path for: {:?}", dir); Ok(Name(vec![NameEntry { filename: Filename::from("/root/just/kidding"), From 9b5936c7187c6695f4ed360eb2aef675906f63ec Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 06:21:31 +1000 Subject: [PATCH 275/393] handling and logging unsupported or uninitialised messages --- sftp/src/sftphandle.rs | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 4aea15ae..b6e698e9 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -142,7 +142,6 @@ where let mut sink = SftpSink::new(buffer_out); - // TODO: Handle gracesfully unknow packets match SftpPacket::decode_request(&mut source) { Ok(request) => { info!("received request: {:?}", request); @@ -150,12 +149,12 @@ where } Err(e) => match e { WireError::UnknownPacket { number } => { - warn!("Unsuported Packet Number {:?} {:?}", number, e); + warn!("Error decoding SFTP Packet:{:?}", e); push_unsuported(ReqId(u32::MAX), &mut sink)?; } _ => { - error!("Could not decode the request: {:?}", e); - return Err(e); + error!("Error decoding SFTP Packet: {:?}", e); + push_unsuported(ReqId(u32::MAX), &mut sink)?; } }, }; @@ -172,7 +171,9 @@ where T: SftpServer, { if !self.initialized && !matches!(request, SftpPacket::Init(_)) { - return Err(WireError::SSHProto); // TODO: Start using the SFTP Errors + push_general_failure(ReqId(u32::MAX), "Not Initialized", sink)?; + error!("Request sent before init: {:?}", request); + return Ok(()); } match request { SftpPacket::Init(_) => { @@ -241,6 +242,25 @@ fn push_unsuported(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireErr lang: "EN".into(), }, ); + debug!("Pushing a unsupported status message: {:?}", response); + response.encode_response(sink)?; + Ok(()) +} + +fn push_general_failure( + req_id: ReqId, + msg: &'static str, + sink: &mut SftpSink<'_>, +) -> Result<(), WireError> { + let response = SftpPacket::Status( + req_id, + Status { + code: StatusCode::SSH_FX_FAILURE, + message: msg.into(), + lang: "EN".into(), + }, + ); + debug!("Pushing a general failure status message: {:?}", response); response.encode_response(sink)?; Ok(()) } From d8447dd46e75da503e607287df94865a065628c0 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 07:35:45 +1000 Subject: [PATCH 276/393] fix bug encoding_request->encoding_response --- sftp/src/sftphandle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index b6e698e9..22c0896e 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -219,7 +219,7 @@ where lang: "EN".into(), }, ); - response.encode_request(req_id, sink)?; + response.encode_response(sink)?; info!("Sending '{:?}'", response); } }; From dd35abafd2071a95c3220ff2881efd5bdc395c66 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 07:37:35 +1000 Subject: [PATCH 277/393] Adding trait implementation - requires instance: WIP - catches default behaviour: Unsupported --- sftp/src/sftpserver.rs | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 72b1593c..c669dcdd 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -10,39 +10,63 @@ pub type SftpOpResult = core::result::Result; /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. pub trait SftpServer { - // type Handle: Into + TryFrom + Debug; type Handle<'a>: Into> + TryFrom> + Debug + Copy; - // TODO flags struct + /// Opens a file or directory for reading/writing async fn open<'a>( + &mut self, filename: &str, attrs: &Attrs, - ) -> SftpOpResult>; + ) -> SftpOpResult> { + log::error!("SftpServer Open operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } /// Close either a file or directory handle - async fn close<'a>(handle: &Self::Handle<'a>) -> SftpOpResult<()>; + async fn close<'a>(&mut self, handle: &Self::Handle<'a>) -> SftpOpResult<()> { + log::error!("SftpServer Close operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } async fn read<'a>( + &mut self, handle: &Self::Handle<'a>, offset: u64, reply: &mut ReadReply, - ) -> SftpOpResult<()>; + ) -> SftpOpResult<()> { + log::error!("SftpServer Read operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } async fn write<'a>( + &mut self, handle: &Self::Handle<'a>, offset: u64, buf: &[u8], - ) -> SftpOpResult<()>; + ) -> SftpOpResult<()> { + log::error!("SftpServer Write operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } - async fn opendir<'a>(dir: &str) -> SftpOpResult>; + async fn opendir<'a>(&mut self, dir: &str) -> SftpOpResult> { + log::error!("SftpServer OpenDir operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } async fn readdir<'a>( + &mut self, handle: &Self::Handle<'a>, reply: &mut DirReply, - ) -> SftpOpResult<()>; + ) -> SftpOpResult<()> { + log::error!("SftpServer ReadDir operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } /// Provides the real path of the directory specified - async fn realpath(dir: &str) -> SftpOpResult>; + async fn realpath(&mut self, dir: &str) -> SftpOpResult> { + log::error!("SftpServer RealPath operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } } pub struct ReadReply<'g, 'a> { From 81680e681b0f11e9ece91a9650a6ef81a920682b Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Sep 2025 15:00:47 +1000 Subject: [PATCH 278/393] Simplifying SftpServer trait async and the Handle are out for now I would like to keep those but since I want a structured of SftpServer to keep it's own internal state, I could not use async or a Handle with a trait definition. It is unfortunately unstable. Still a WIP but now a client can PUT a small file and its content will be displayed in debug. For now the buffers overflow on long SSH_FXP_WRITE packets and fail after trying to decode in the next loop iteration more of the same packet content. The previous sftpserver trait has been stored in asyncsftpserver.rs --- demo/sftp/std/src/demosftpserver.rs | 140 ++++++++++++++++++++-------- demo/sftp/std/src/main.rs | 5 +- sftp/src/asyncsftpserver.rs | 87 +++++++++++++++++ sftp/src/lib.rs | 1 - sftp/src/sftphandle.rs | 110 +++++++++++++--------- sftp/src/sftpserver.rs | 68 ++++++++------ 6 files changed, 297 insertions(+), 114 deletions(-) create mode 100644 sftp/src/asyncsftpserver.rs diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 19fd22dd..bb846ab9 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -1,3 +1,4 @@ +use sunset::sshwire::BinString; use sunset_sftp::{ Attrs, DirReply, FileHandle, Filename, Name, NameEntry, ReadReply, SftpOpResult, SftpServer, StatusCode, @@ -6,65 +7,124 @@ use sunset_sftp::{ #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -pub struct DemoSftpServer {} - -impl SftpServer for DemoSftpServer { - type Handle<'a> = FileHandle<'a>; +pub struct DemoSftpServer { + valid_handlers: Vec, + user_path: String, +} - async fn open<'a>( +impl DemoSftpServer { + pub fn new(user: String) -> Self { + DemoSftpServer { + valid_handlers: vec![], + user_path: format!("/{}/", user.clone()), + } + } +} +impl SftpServer<'_> for DemoSftpServer { + // Mocking an Open operation. Will not check for permissions + fn open( + &mut self, filename: &str, - // flags: u32, - attrs: &Attrs, - ) -> SftpOpResult> { - warn!("Wont allow open!"); - Err(StatusCode::SSH_FX_PERMISSION_DENIED) + _attrs: &Attrs, + ) -> SftpOpResult> { + if self.valid_handlers.contains(&filename.to_string()) { + warn!("File {:?} already open, won't allow it", filename); + return Err(StatusCode::SSH_FX_PERMISSION_DENIED); + } + + self.valid_handlers.push(filename.to_string()); + + let fh = FileHandle(BinString( + self.valid_handlers.last().expect("just pushed an element").as_bytes(), + )); + Ok(fh) + } + + fn realpath(&mut self, dir: &str) -> SftpOpResult> { + debug!("finding path for: {:?}", dir); + Ok(Name(vec![NameEntry { + filename: Filename::from(self.user_path.as_str()), + _longname: Filename::from(""), + attrs: Attrs { + size: None, + uid: None, + gid: None, + permissions: None, + atime: None, + mtime: None, + ext_count: None, + }, + }])) } - async fn close<'a>(handle: &Self::Handle<'a>) -> SftpOpResult<()> { - todo!() + fn close(&mut self, handle: &FileHandle) -> SftpOpResult<()> { + let initial_count = self.valid_handlers.len(); + if initial_count == 0 { + log::error!( + "SftpServer Close operation with no handles stored: handle = {:?}", + handle + ); + return Err(StatusCode::SSH_FX_FAILURE); + } + + let filename = + String::from_utf8(handle.0.as_ref().to_vec()).unwrap_or("".into()); + + if !self.valid_handlers.contains(&filename) { + log::error!( + "SftpServer Close operation could not match an stored handler: handle = {:?}", + handle + ); + return Err(StatusCode::SSH_FX_FAILURE); + } + self.valid_handlers.retain(|handler| handler.ne(&filename)); + log::debug!("SftpServer Close operation on {:?} was successful", filename); + Ok(()) } - async fn read<'a>( - handle: &Self::Handle<'a>, + fn read( + &mut self, + handle: &FileHandle, offset: u64, reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { - todo!() + log::error!( + "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", + handle, + offset + ); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - async fn write<'a>( - handle: &Self::Handle<'a>, + fn write( + &mut self, + handle: &FileHandle, offset: u64, buf: &[u8], ) -> SftpOpResult<()> { - todo!() + log::debug!( + "SftpServer Write operation: handle = {:?}, offset = {:?}, buf = {:?}", + handle, + offset, + String::from_utf8(buf.to_vec()) + ); + Ok(()) } - async fn opendir<'a>(dir: &str) -> SftpOpResult> { - todo!() + fn opendir(&mut self, dir: &str) -> SftpOpResult> { + log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - async fn readdir<'a>( - handle: &Self::Handle<'a>, + fn readdir( + &mut self, + handle: &FileHandle, reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { - todo!() - } - - async fn realpath(dir: &str) -> SftpOpResult> { - debug!("finding path for: {:?}", dir); - Ok(Name(vec![NameEntry { - filename: Filename::from("/root/just/kidding"), - _longname: Filename::from(""), - attrs: Attrs { - size: None, - uid: None, - gid: None, - permissions: None, - atime: None, - mtime: None, - ext_count: None, - }, - }])) + log::error!( + "SftpServer ReadDir operation not defined: handle = {:?}", + handle + ); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } } diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index e59e8c92..5309e641 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -172,8 +172,9 @@ async fn sftp_server_loop( ch: ChanHandle, ) -> Result<(), Error> { let mut stdio = serv.stdio(ch).await?; - let mut sftp_handler = - SftpHandler::::new(&buffer_in, &mut buffer_out); + let mut file_server = DemoSftpServer::new("user".to_string()); + + let mut sftp_handler = SftpHandler::new(&mut file_server); Ok(loop { let lr = stdio.read(&mut buffer_in).await?; debug!("SFTP <---- received: {:?}", &buffer_in[0..lr]); diff --git a/sftp/src/asyncsftpserver.rs b/sftp/src/asyncsftpserver.rs new file mode 100644 index 00000000..b2bd93e8 --- /dev/null +++ b/sftp/src/asyncsftpserver.rs @@ -0,0 +1,87 @@ +use crate::proto::{Attrs, FileHandle, Name, StatusCode}; + +use core::marker::PhantomData; + +pub type SftpOpResult = core::result::Result; + +/// All trait functions are optional in the SFTP protocol. +/// Some less core operations have a Provided implementation returning +/// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, +/// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. +pub trait AsyncSftpServer { + type Handle<'a>: Into> + TryFrom> + Debug + Copy; + + /// Opens a file or directory for reading/writing + async fn open<'a>( + filename: &str, + attrs: &Attrs, + ) -> SftpOpResult> { + log::error!("SftpServer Open operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + /// Close either a file or directory handle + async fn close<'a>(&mut self, handle: &FileHandle<'a>) -> SftpOpResult<()> { + log::error!("SftpServer Close operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + async fn read<'a>( + handle: &FileHandle<'a>, + offset: u64, + reply: &mut ReadReply<'_, '_>, + ) -> SftpOpResult<()> { + log::error!("SftpServer Read operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + async fn write<'a>( + handle: &FileHandle<'a>, + offset: u64, + buf: &[u8], + ) -> SftpOpResult<()> { + log::error!("SftpServer Write operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + async fn opendir<'a>(&mut self, dir: &str) -> SftpOpResult> { + log::error!("SftpServer OpenDir operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + async fn readdir<'a>( + handle: &FileHandle<'a>, + reply: &mut DirReply<'_, '_>, + ) -> SftpOpResult<()> { + log::error!("SftpServer ReadDir operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + /// Provides the real path of the directory specified + async fn realpath(&mut self, dir: &str) -> SftpOpResult> { + log::error!("SftpServer RealPath operation not defined"); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } +} + +pub struct ReadReply<'g, 'a> { + chan: ChanOut<'g, 'a>, +} + +impl<'g, 'a> ReadReply<'g, 'a> { + pub async fn reply(self, data: &[u8]) {} +} + +pub struct DirReply<'g, 'a> { + chan: ChanOut<'g, 'a>, +} + +impl<'g, 'a> DirReply<'g, 'a> { + pub async fn reply(self, data: &[u8]) {} +} + +// TODO: Implement correct Channel Out +pub struct ChanOut<'g, 'a> { + _phantom_g: PhantomData<&'g ()>, + _phantom_a: PhantomData<&'a ()>, +} diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 1501b72d..5e7d91ba 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,5 +1,4 @@ mod proto; -mod sftperror; mod sftphandle; mod sftpserver; diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 22c0896e..e7e1de76 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -6,7 +6,6 @@ use crate::sftpserver::SftpServer; use sunset::sshwire::{SSHDecode, SSHSink, SSHSource, WireError, WireResult}; -use core::marker::PhantomData; use core::u32; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; @@ -101,26 +100,18 @@ impl<'g> SSHSink for SftpSink<'g> { } } -#[derive(Debug, Clone)] -pub struct SftpHandler<'a, T> -where - T: SftpServer, -{ - server_type: PhantomData, - handle_list: Vec>, +//#[derive(Debug, Clone)] +pub struct SftpHandler<'a> { + file_server: &'a mut dyn SftpServer<'a>, initialized: bool, } -impl<'a, T> SftpHandler<'a, T> -where - T: SftpServer, -{ - pub fn new(buffer_in: &[u8], buffer_out: &mut [u8]) -> Self { - SftpHandler { - server_type: PhantomData, - handle_list: vec![], - initialized: false, - } +impl<'a> SftpHandler<'a> { + pub fn new( + file_server: &'a mut impl SftpServer<'a>, + // max_file_handlers: u32 + ) -> Self { + SftpHandler { file_server, initialized: false } } /// Decodes the buffer_in request, process the request delegating operations to an Struct implementing SftpServer, @@ -148,13 +139,13 @@ where self.process_known_request(&mut sink, request).await?; } Err(e) => match e { - WireError::UnknownPacket { number } => { + WireError::UnknownPacket { number: _ } => { warn!("Error decoding SFTP Packet:{:?}", e); - push_unsuported(ReqId(u32::MAX), &mut sink)?; + push_unsupported(ReqId(u32::MAX), &mut sink)?; } _ => { error!("Error decoding SFTP Packet: {:?}", e); - push_unsuported(ReqId(u32::MAX), &mut sink)?; + push_unsupported(ReqId(u32::MAX), &mut sink)?; } }, }; @@ -166,10 +157,7 @@ where &mut self, sink: &mut SftpSink<'_>, request: SftpPacket<'_>, - ) -> Result<(), WireError> - where - T: SftpServer, - { + ) -> Result<(), WireError> { if !self.initialized && !matches!(request, SftpPacket::Init(_)) { push_general_failure(ReqId(u32::MAX), "Not Initialized", sink)?; error!("Request sent before init: {:?}", request); @@ -189,20 +177,19 @@ where } SftpPacket::PathInfo(req_id, path_info) => { let a_name = - T::realpath(path_info.path.as_str().expect( - "Could not deref and the errors are not harmonised", - )) - .await - .expect("Could not deref and the errors are not harmonised"); + self.file_server + .realpath(path_info.path.as_str().expect( + "Could not deref and the errors are not harmonized", + )) + .expect("Could not deref and the errors are not harmonized"); let response = SftpPacket::Name(req_id, a_name); response.encode_response(sink)?; } SftpPacket::Open(req_id, open) => { - match T::open(open.filename.as_str()?, &open.attrs).await { + match self.file_server.open(open.filename.as_str()?, &open.attrs) { Ok(handle) => { - self.handle_list.push(handle.into()); let response = SftpPacket::Handle( req_id, proto::Handle { handle: handle.into() }, @@ -211,29 +198,61 @@ where info!("Sending '{:?}'", response); } Err(status_code) => { - let response = SftpPacket::Status( - req_id, - Status { - code: status_code, - message: "".into(), - lang: "EN".into(), - }, - ); - response.encode_response(sink)?; - info!("Sending '{:?}'", response); + error!("Open failed: {:?}", status_code); + push_general_failure(req_id, "", sink)?; + } + }; + } + SftpPacket::Write(req_id, write) => { + match self.file_server.write( + &write.handle, + write.offset, + write.data.as_ref(), + ) { + Ok(_) => push_ok(req_id, sink)?, + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure(req_id, "error writing ", sink)? } }; - // push_unsuported(req_id, sink)?; + } + SftpPacket::Close(req_id, close) => { + match self.file_server.close(&close.handle) { + Ok(_) => push_ok(req_id, sink)?, + Err(e) => { + error!("SFTP Close thrown: {:?}", e); + push_general_failure(req_id, "", sink)? + } + } } _ => { - push_unsuported(ReqId(0), sink)?; + push_unsupported(ReqId(0), sink)?; } }; Ok(()) } } -fn push_unsuported(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireError> { +#[inline] +fn push_ok(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireError> { + let response = SftpPacket::Status( + req_id, + Status { + code: StatusCode::SSH_FX_OK, + message: "".into(), + lang: "EN".into(), + }, + ); + debug!("Pushing an OK status message: {:?}", response); + response.encode_response(sink)?; + Ok(()) +} + +#[inline] +fn push_unsupported( + req_id: ReqId, + sink: &mut SftpSink<'_>, +) -> Result<(), WireError> { let response = SftpPacket::Status( req_id, Status { @@ -247,6 +266,7 @@ fn push_unsuported(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireErr Ok(()) } +#[inline] fn push_general_failure( req_id: ReqId, msg: &'static str, diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index c669dcdd..4c7f1fbf 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,6 +1,5 @@ use crate::proto::{Attrs, FileHandle, Name, StatusCode}; -use core::fmt::Debug; use core::marker::PhantomData; pub type SftpOpResult = core::result::Result; @@ -9,62 +8,79 @@ pub type SftpOpResult = core::result::Result; /// Some less core operations have a Provided implementation returning /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. -pub trait SftpServer { - type Handle<'a>: Into> + TryFrom> + Debug + Copy; +pub trait SftpServer<'a> { + // type Handle<'a>: Into> + TryFrom> + Debug + Copy; /// Opens a file or directory for reading/writing - async fn open<'a>( + fn open( &mut self, filename: &str, attrs: &Attrs, - ) -> SftpOpResult> { - log::error!("SftpServer Open operation not defined"); + ) -> SftpOpResult> { + log::error!( + "SftpServer Open operation not defined: filename = {:?}, attrs = {:?}", + filename, + attrs + ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } /// Close either a file or directory handle - async fn close<'a>(&mut self, handle: &Self::Handle<'a>) -> SftpOpResult<()> { - log::error!("SftpServer Close operation not defined"); + fn close(&mut self, handle: &FileHandle) -> SftpOpResult<()> { + log::error!("SftpServer Close operation not defined: handle = {:?}", handle); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - async fn read<'a>( + fn read( &mut self, - handle: &Self::Handle<'a>, + handle: &FileHandle, offset: u64, - reply: &mut ReadReply, + reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { - log::error!("SftpServer Read operation not defined"); + log::error!( + "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", + handle, + offset + ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - async fn write<'a>( + fn write( &mut self, - handle: &Self::Handle<'a>, + handle: &FileHandle, offset: u64, buf: &[u8], ) -> SftpOpResult<()> { - log::error!("SftpServer Write operation not defined"); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + log::error!( + "SftpServer Write operation: handle = {:?}, offset = {:?}, buf = {:?}", + handle, + offset, + String::from_utf8(buf.to_vec()) + ); + Ok(()) } - async fn opendir<'a>(&mut self, dir: &str) -> SftpOpResult> { - log::error!("SftpServer OpenDir operation not defined"); + fn opendir(&mut self, dir: &str) -> SftpOpResult> { + log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - async fn readdir<'a>( + fn readdir( &mut self, - handle: &Self::Handle<'a>, - reply: &mut DirReply, + handle: &FileHandle, + reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { - log::error!("SftpServer ReadDir operation not defined"); + log::error!( + "SftpServer ReadDir operation not defined: handle = {:?}", + handle + ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } /// Provides the real path of the directory specified - async fn realpath(&mut self, dir: &str) -> SftpOpResult> { - log::error!("SftpServer RealPath operation not defined"); + fn realpath(&mut self, dir: &str) -> SftpOpResult> { + log::error!("SftpServer RealPath operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } } @@ -74,7 +90,7 @@ pub struct ReadReply<'g, 'a> { } impl<'g, 'a> ReadReply<'g, 'a> { - pub async fn reply(self, data: &[u8]) {} + pub fn reply(self, data: &[u8]) {} } pub struct DirReply<'g, 'a> { @@ -82,7 +98,7 @@ pub struct DirReply<'g, 'a> { } impl<'g, 'a> DirReply<'g, 'a> { - pub async fn reply(self, data: &[u8]) {} + pub fn reply(self, data: &[u8]) {} } // TODO: Implement correct Channel Out From 8ae10e864ee7ff141f8e823719b6f6b07b74a468 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 10 Sep 2025 11:06:34 +1000 Subject: [PATCH 279/393] Simple random data files generation for big write testing --- .../sftp/std/testing/test_only_file_upload.sh | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100755 demo/sftp/std/testing/test_only_file_upload.sh diff --git a/demo/sftp/std/testing/test_only_file_upload.sh b/demo/sftp/std/testing/test_only_file_upload.sh new file mode 100755 index 00000000..eec48750 --- /dev/null +++ b/demo/sftp/std/testing/test_only_file_upload.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Generate random data files +echo "Generating random data files..." +dd if=/dev/random bs=128 count=1 of=./128B_random 2>/dev/null +dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null +dd if=/dev/random bs=512 count=4 of=./2kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=1024 of=./1MB_random 2>/dev/null + + +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." +echo "Test Results:" +echo "=============" + +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +put ./128B_random +put ./512B_random +put ./2kB_random +put ./1MB_random +bye +EOF + + +if [ $? -eq 0 ]; then + echo "PASS" +else + echo "FAIL" +fi + +echo "Cleaning up local files..." +rm -f ./*_random + +echo "Upload test completed." \ No newline at end of file From 1972940e52c51091ec37fa382d04bc0e54cfc475 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 10 Sep 2025 16:40:02 +1000 Subject: [PATCH 280/393] Adding SFTP Packet Header and SSH_FXP_WRITE offset definitions --- sftp/src/proto.rs | 47 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 65125ca6..27eb6a64 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -8,6 +8,30 @@ use sunset_sshwire_derive::{SSHDecode, SSHEncode}; use log::{debug, error, info, log, trace, warn}; use num_enum::FromPrimitive; use paste::paste; + +/// SFTP Minimum packet length is 9 bytes corresponding with `SSH_FXP_INIT` +pub const SFTP_MINIMUM_PACKET_LEN: usize = 9; + +/// SFTP packets have the packet type after a u32 length field +pub const SFTP_FIELD_ID_INDEX: usize = 4; +/// SFTP packets ID length is 1 byte +pub const SFTP_FIELD_ID_LEN: usize = 1; +/// SFTP packets start with the length field +pub const SFTP_FIELD_LEN_INDEX: usize = 0; +/// SFTP packets length field us u32 +pub const SFTP_FIELD_LEN_LENGTH: usize = 4; + +// SSH_FXP_WRITE SFTP Packet definition used to decode long packets that do not fit in one buffer + +/// SFTP SSH_FXP_WRITE Packet cannot be shorter than this (len:4+pnum:1+rid:4+hand:4+0+data:4+0 bytes = 17 bytes) [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) +pub const SFTP_MINIMUM_WRITE_PACKET_LEN: usize = 17; + +/// SFTP SSH_FXP_WRITE Packet request id field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) +pub const SFTP_WRITE_REQID_INDEX: usize = 5; + +/// SFTP SSH_FXP_WRITE Packet handle field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) +pub const SFTP_WRITE_HANDLE_INDEX: usize = 9; + // TODO is utf8 enough, or does this need to be an opaque binstring? #[derive(Debug, SSHEncode, SSHDecode)] pub struct Filename<'a>(TextString<'a>); @@ -590,15 +614,22 @@ macro_rules! sftpmessages { 'de: 'a { - let sftp_packet = Self::dec(s)? ; - - if (!sftp_packet.sftp_num().is_request() - && !sftp_packet.sftp_num().is_init()) - { - return Err(WireError::PacketWrong) + // let sftp_packet = Self::dec(s)?; + match Self::dec(s) { + Ok(sftp_packet)=> { + if (!sftp_packet.sftp_num().is_request() + && !sftp_packet.sftp_num().is_init()) + { + Err(WireError::PacketWrong) + }else{ + Ok(sftp_packet) + + } + }, + Err(e) => { + Err(e) + } } - - Ok(sftp_packet) } /// Encode a response. From 569d033381dfeb70a6926023d4031116316cda4c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 10 Sep 2025 16:41:09 +1000 Subject: [PATCH 281/393] Simplified Main Demo SFTP loop and reduced the SFTP buffers size --- demo/sftp/std/src/main.rs | 54 +++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 5309e641..c6e3ebf9 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -144,10 +144,33 @@ impl DemoServer for StdDemo { info!("SFTP loop has received a channel handle {:?}", ch.num()); - let buffer_in = [0u8; 1000]; - let buffer_out = [0u8; 1000]; + let mut buffer_in = [0u8; 512]; + let mut buffer_out = [0u8; 512]; + + match { + let mut stdio = serv.stdio(ch).await?; + let mut file_server = DemoSftpServer::new("user".to_string()); + + let mut sftp_handler = + SftpHandler::new(&mut file_server, buffer_in.len()); + loop { + let lr = stdio.read(&mut buffer_in).await?; + trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); + if lr == 0 { + debug!("client disconnected"); + break; + } - match sftp_server_loop(serv, buffer_in, buffer_out, ch).await { + let lw = sftp_handler + .process(&buffer_in[0..lr], &mut buffer_out) + .await?; + if lw > 0 { + stdio.write(&mut buffer_out[0..lw]).await?; + trace!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); + } + } + Ok::<_, Error>(()) + } { Ok(_) => { warn!("sftp server loop finished gracefully"); } @@ -165,31 +188,6 @@ impl DemoServer for StdDemo { } } -async fn sftp_server_loop( - serv: &SSHServer<'_>, - mut buffer_in: [u8; 1000], - mut buffer_out: [u8; 1000], - ch: ChanHandle, -) -> Result<(), Error> { - let mut stdio = serv.stdio(ch).await?; - let mut file_server = DemoSftpServer::new("user".to_string()); - - let mut sftp_handler = SftpHandler::new(&mut file_server); - Ok(loop { - let lr = stdio.read(&mut buffer_in).await?; - debug!("SFTP <---- received: {:?}", &buffer_in[0..lr]); - if lr == 0 { - debug!("client disconnected"); - return Ok(()); - } - - let lw = sftp_handler.process(&buffer_in[0..lr], &mut buffer_out).await?; - - stdio.write(&mut buffer_out[0..lw]).await?; - debug!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); - }) -} - // TODO: pool_size should be NUM_LISTENERS but needs a literal #[embassy_executor::task(pool_size = 4)] async fn listen( From fcfd0aa9eff8ac8387fd2ea54f1a97b6fed8a1d1 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 10 Sep 2025 16:44:47 +1000 Subject: [PATCH 282/393] WIP: Working on long WRITE packets processing --- sftp/src/sftphandle.rs | 159 +++++++++++++++++++++++++++++++++++------ 1 file changed, 137 insertions(+), 22 deletions(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index e7e1de76..2659b25b 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,6 +1,7 @@ use crate::proto::{ - self, FileHandle, InitVersionLowest, ReqId, SFTP_VERSION, SftpPacket, Status, - StatusCode, + self, Handle, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, + SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, SFTP_MINIMUM_PACKET_LEN, + SFTP_VERSION, SFTP_WRITE_REQID_INDEX, SftpNum, SftpPacket, Status, StatusCode, }; use crate::sftpserver::SftpServer; @@ -9,7 +10,10 @@ use sunset::sshwire::{SSHDecode, SSHSink, SSHSource, WireError, WireResult}; use core::u32; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; +use std::usize; +/// This implementation is an extension of the SSHSource interface to handle some challenges with SFTP packets +/// #[derive(Default, Debug)] pub struct SftpSource<'de> { pub buffer: &'de [u8], @@ -19,7 +23,7 @@ pub struct SftpSource<'de> { impl<'de> SSHSource<'de> for SftpSource<'de> { fn take(&mut self, len: usize) -> sunset::sshwire::WireResult<&'de [u8]> { if len + self.index > self.buffer.len() { - return Err(WireError::NoRoom); + return Err(WireError::RanOut); } let original_index = self.index; let slice = &self.buffer[self.index..self.index + len]; @@ -42,15 +46,80 @@ impl<'de> SSHSource<'de> for SftpSource<'de> { } } -// // This implementation is an extension of the SSHSource interface. -// impl<'de> SftpSource<'de> { -// /// Rewinds the index back to the initial byte -// /// -// /// In case of an error deserialising the SSHSource it allows reprocesing the buffer from start -// pub fn rewind(&mut self) -> () { -// self.index = 0; -// } -// } +impl<'de> SftpSource<'de> { + pub fn new(buffer: &'de [u8]) -> Self { + SftpSource { buffer: buffer, index: 0 } + } + + /// Rewinds the index back to the initial byte + /// + /// In case of an error deserializing the SSHSource it allows reprocessing the buffer from start + pub fn rewind(&mut self) -> () { + self.index = 0; + } + + /// Peaks the buffer for packet type. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: will only work in well formed packets, in other case the result will contain garbage + fn peak_packet_type(&self) -> WireResult { + // const SFTP_ID_BUFFER_INDEX: usize = 4; // All SFTP packet have the packet type after a u32 length field + // const SFTP_MINIMUM_LENGTH: usize = 9; // Corresponds to a minimal SSH_FXP_INIT packet + if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + Err(WireError::PacketWrong) + } else { + Ok(SftpNum::from(self.buffer[SFTP_FIELD_ID_INDEX])) + } + } + + /// Peaks the buffer for packet length. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: will only work in well formed packets, in other case the result will contain garbage + fn peak_packet_len(&self) -> WireResult { + if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + Err(WireError::PacketWrong) + } else { + let mut raw_bytes = [0u8; 4]; + raw_bytes.copy_from_slice( + &self.buffer[SFTP_FIELD_LEN_INDEX + ..SFTP_FIELD_LEN_INDEX + SFTP_FIELD_LEN_LENGTH], + ); + + Ok(u32::from_be_bytes(raw_bytes)) + } + } + + /// Assuming that the buffer contains a Write request packet, Peaks the buffer for the handle length. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: will only work in well formed write packets, in other case the result will contain garbage + fn peak_write_handle_offset_n_data_len( + &mut self, + ) -> WireResult<(Handle<'_>, ReqId, u64, u32)> { + if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + Err(WireError::PacketWrong) + } else { + let prev_index = self.index; + self.index = SFTP_WRITE_REQID_INDEX; + let req_id = ReqId::dec(self)?; + let handle = Handle::dec(self)?; + let offset = u64::dec(self)?; + let data_len = u32::dec(self)?; + + self.index = prev_index; + + debug!( + "Request ID = {:?}, Handle = {:?}, offset = {:?}, data length = {:?}, ", + req_id, handle, offset, data_len + ); + Ok((handle, req_id, offset, data_len)) + } + } +} #[derive(Default)] pub struct SftpSink<'g> { @@ -59,21 +128,19 @@ pub struct SftpSink<'g> { } impl<'g> SftpSink<'g> { - const LENG_FIELD_LEN: usize = 4; // TODO: Move it to a better location - pub fn new(s: &'g mut [u8]) -> Self { - SftpSink { buffer: s, index: Self::LENG_FIELD_LEN } + SftpSink { buffer: s, index: SFTP_FIELD_LEN_LENGTH } } /// Finalise the buffer by prepending the payload size and returning /// /// Returns the final index in the buffer as a reference for the space used pub fn finalise(&mut self) -> usize { - if self.index <= SftpSink::LENG_FIELD_LEN { + if self.index <= SFTP_FIELD_LEN_LENGTH { warn!("SftpSink trying to terminate it before pushing data"); return 0; } // size is 0 - let used_size = (self.index - 4) as u32; + let used_size = (self.index - SFTP_FIELD_LEN_LENGTH) as u32; used_size .to_be_bytes() @@ -103,15 +170,27 @@ impl<'g> SSHSink for SftpSink<'g> { //#[derive(Debug, Clone)] pub struct SftpHandler<'a> { file_server: &'a mut dyn SftpServer<'a>, + buffer_in_len: usize, initialized: bool, + long_packet: bool, } impl<'a> SftpHandler<'a> { pub fn new( file_server: &'a mut impl SftpServer<'a>, - // max_file_handlers: u32 + buffer_len: usize, // max_file_handlers: u32 ) -> Self { - SftpHandler { file_server, initialized: false } + if buffer_len < 256 { + warn!( + "Buffer length too small, must be at least 256 bytes. You are in uncharted territory" + ) + } + SftpHandler { + file_server, + buffer_in_len: buffer_len, + long_packet: false, + initialized: false, + } } /// Decodes the buffer_in request, process the request delegating operations to an Struct implementing SftpServer, @@ -121,15 +200,17 @@ impl<'a> SftpHandler<'a> { buffer_in: &[u8], buffer_out: &mut [u8], ) -> WireResult { - if buffer_in.len() < 4 { + let in_len = buffer_in.len(); + debug!("Received {:} bytes to process", in_len); + if !self.long_packet & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) { return Err(WireError::PacketWrong); } - let mut source = SftpSource { buffer: buffer_in, index: 0 }; + let mut source = SftpSource::new(buffer_in); trace!("Source content: {:?}", source); let packet_length = u32::dec(&mut source)?; - trace!("Packet field lenght content: {}", packet_length); + trace!("Packet field length content: {}", packet_length); let mut sink = SftpSink::new(buffer_out); @@ -139,6 +220,40 @@ impl<'a> SftpHandler<'a> { self.process_known_request(&mut sink, request).await?; } Err(e) => match e { + WireError::RanOut => { + warn!( + "RanOut for the SFTP Packet in the source buffer: {:?}", + e + ); + source.rewind(); // Not strictly required + let packet_total_length = source.peak_packet_len()?; + let packet_type = source.peak_packet_type()?; + match packet_type { + SftpNum::SSH_FXP_WRITE => { + self.long_packet = true; + let (file_handle, req_id, offset, data_len) = + source.peak_write_handle_offset_n_data_len()?; + warn!( + "We got a long Write packet. Excellent! total len = {:?}, type = {:?}, req_id = {:?}, handle = {:?}, offset = {:?}, data_len = {:?}", + packet_total_length, + packet_type, + req_id, + file_handle, + offset, + data_len + ); + } + _ => { + error!( + "We do not know how to handle this long packet: {:?}", + packet_type + ); + todo!( + " Push a general failure with the request ID and RanOut comment" + ); + } + }; + } WireError::UnknownPacket { number: _ } => { warn!("Error decoding SFTP Packet:{:?}", e); push_unsupported(ReqId(u32::MAX), &mut sink)?; From 190d38b90be73d68fcdeb8d65ff9904ce0a471cb Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 12 Sep 2025 13:46:26 +1000 Subject: [PATCH 283/393] WIP: Processing long Write Requests For now the flow of receiving SFTP Write request longer than the buffer is going in the right direction. I have been able to process up to 64kB of data. Still this is not complete since I need to: - Write the data received and check that the data sent and received has not been altered - Issue: Writes with files over 64kB fail - Reshape the file handle exchanged from the demoserver so it has a fixed length and it is compact - Refactor sftphandle --- demo/sftp/std/src/demosftpserver.rs | 2 +- demo/sftp/std/src/main.rs | 1 - .../sftp/std/testing/test_only_file_upload.sh | 18 +- sftp/src/proto.rs | 3 +- sftp/src/sftphandle.rs | 310 +++++++++++++----- sftp/src/sftpserver.rs | 6 + 6 files changed, 254 insertions(+), 86 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index bb846ab9..f1ad124e 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -8,7 +8,7 @@ use sunset_sftp::{ use log::{debug, error, info, log, trace, warn}; pub struct DemoSftpServer { - valid_handlers: Vec, + valid_handlers: Vec, // TODO: Obscure the handlers user_path: String, } diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index c6e3ebf9..8d98977c 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -36,7 +36,6 @@ async fn net_task(mut runner: embassy_net::Runner<'static, TunTapDevice>) -> ! { #[embassy_executor::task] async fn main_task(spawner: Spawner) { - // TODO config let opt_tap0 = "tap0"; let ip4 = "192.168.69.2"; let cir = 24; diff --git a/demo/sftp/std/testing/test_only_file_upload.sh b/demo/sftp/std/testing/test_only_file_upload.sh index eec48750..97f93269 100755 --- a/demo/sftp/std/testing/test_only_file_upload.sh +++ b/demo/sftp/std/testing/test_only_file_upload.sh @@ -8,7 +8,11 @@ REMOTE_USER="any" echo "Generating random data files..." dd if=/dev/random bs=128 count=1 of=./128B_random 2>/dev/null dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null -dd if=/dev/random bs=512 count=4 of=./2kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=2 of=./2kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=4 of=./4kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null # OK +dd if=/dev/random bs=1024 count=65 of=./MaxMaybe_random 2>/dev/null # Fails dd if=/dev/random bs=1024 count=1024 of=./1MB_random 2>/dev/null @@ -16,11 +20,15 @@ echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." echo "Test Results:" echo "=============" +# put ./128B_random +# put ./512B_random +# put ./2kB_random +# put ./4kB_random +# put ./16kB_random +# put ./1MB_random sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF -put ./128B_random -put ./512B_random -put ./2kB_random -put ./1MB_random +put ./64kB_random +put ./MaxMaybe_random bye EOF diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 27eb6a64..17d21a96 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -593,8 +593,7 @@ macro_rules! sftpmessages { if !num.is_response() { return Err(WireError::PacketWrong) // return error::SSHProto.fail(); - // TODO: Not an error in the SSHProtocol rather the SFTP. - // Maybe is time to define an SftpError + // TODO: Not an error in the SSHProtocol rather the SFTP Protocol. } let id = ReqId(u32::dec(s)?); diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 2659b25b..42ce44ec 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,19 +1,63 @@ +use crate::FileHandle; use crate::proto::{ - self, Handle, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, - SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, SFTP_MINIMUM_PACKET_LEN, - SFTP_VERSION, SFTP_WRITE_REQID_INDEX, SftpNum, SftpPacket, Status, StatusCode, + self, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, + SFTP_FIELD_LEN_LENGTH, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, + SFTP_WRITE_REQID_INDEX, SftpNum, SftpPacket, Status, StatusCode, }; -use crate::sftpserver::SftpServer; +use crate::sftpserver::{FILE_HANDLE_MAX_LEN, SftpServer}; -use sunset::sshwire::{SSHDecode, SSHSink, SSHSource, WireError, WireResult}; +use sunset::sshwire::{ + BinString, SSHDecode, SSHSink, SSHSource, WireError, WireResult, +}; use core::u32; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; use std::usize; -/// This implementation is an extension of the SSHSource interface to handle some challenges with SFTP packets -/// +/// Used to keep record of a long SFTP Write request that does not fit in receiving buffer and requires processing in batches +#[derive(Debug)] +struct PartialWriteRequestTracker { + req_id: ReqId, + file_handle: [u8; FILE_HANDLE_MAX_LEN], // TODO: Change the file handle in SftpServer functions signature so it has a sort fixed length handle. + file_handle_len: usize, + data_len: u32, + remain_data_len: u32, + remain_data_offset: u64, +} + +impl PartialWriteRequestTracker { + pub fn new( + req_id: ReqId, + file_handle: &FileHandle<'_>, + data_len: u32, + remain_data_len: u32, + remain_data_offset: u64, + ) -> WireResult { + let handle_data = file_handle.0.as_ref(); + if handle_data.len() > FILE_HANDLE_MAX_LEN { + return Err(WireError::PacketWrong); // Handle too large + } + let mut ret = PartialWriteRequestTracker { + req_id, + file_handle: [0u8; FILE_HANDLE_MAX_LEN], + file_handle_len: handle_data.len(), + data_len, + remain_data_len, + remain_data_offset, + }; + ret.file_handle[..ret.file_handle_len] + .copy_from_slice(file_handle.0.as_ref()); + + Ok(ret) + } + + pub fn get_file_handle(&self) -> FileHandle<'_> { + FileHandle(BinString(&self.file_handle[..self.file_handle_len])) + } +} + +/// SftpSource implements SSHSource and also extra functions to handle some challenges with long SFTP packets in constrained environments #[derive(Default, Debug)] pub struct SftpSource<'de> { pub buffer: &'de [u8], @@ -21,6 +65,7 @@ pub struct SftpSource<'de> { } impl<'de> SSHSource<'de> for SftpSource<'de> { + // Original take fn take(&mut self, len: usize) -> sunset::sshwire::WireResult<&'de [u8]> { if len + self.index > self.buffer.len() { return Err(WireError::RanOut); @@ -40,9 +85,7 @@ impl<'de> SSHSource<'de> for SftpSource<'de> { } fn ctx(&mut self) -> &mut sunset::packets::ParseContext { - todo!( - "I don't know what to do with the ctx, since sftp does not have context" - ); + todo!("Which context for sftp?"); } } @@ -51,13 +94,6 @@ impl<'de> SftpSource<'de> { SftpSource { buffer: buffer, index: 0 } } - /// Rewinds the index back to the initial byte - /// - /// In case of an error deserializing the SSHSource it allows reprocessing the buffer from start - pub fn rewind(&mut self) -> () { - self.index = 0; - } - /// Peaks the buffer for packet type. This does not advance the reading index /// /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail @@ -92,33 +128,60 @@ impl<'de> SftpSource<'de> { } } - /// Assuming that the buffer contains a Write request packet, Peaks the buffer for the handle length. This does not advance the reading index + /// Assuming that the buffer contains a Write request packet initial bytes, Peaks the buffer for the handle length. This does not advance the reading index /// /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail /// /// **Warning**: will only work in well formed write packets, in other case the result will contain garbage - fn peak_write_handle_offset_n_data_len( + fn get_packet_partial_write_content_and_tracker( &mut self, - ) -> WireResult<(Handle<'_>, ReqId, u64, u32)> { + ) -> WireResult<( + FileHandle<'de>, + ReqId, + u64, + BinString<'de>, + PartialWriteRequestTracker, + )> { if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { Err(WireError::PacketWrong) } else { let prev_index = self.index; self.index = SFTP_WRITE_REQID_INDEX; let req_id = ReqId::dec(self)?; - let handle = Handle::dec(self)?; + let file_handle = FileHandle::dec(self)?; let offset = u64::dec(self)?; let data_len = u32::dec(self)?; + let data_len_in_buffer = self.buffer.len() - self.index; + let data_in_buffer = BinString(self.take(data_len_in_buffer)?); + self.index = prev_index; - debug!( - "Request ID = {:?}, Handle = {:?}, offset = {:?}, data length = {:?}, ", - req_id, handle, offset, data_len + let remain_data_len = data_len - data_len_in_buffer as u32; + let remain_data_offset = offset + data_len_in_buffer as u64; + trace!( + "Request ID = {:?}, Handle = {:?}, offset = {:?}, data length in buffer = {:?}, data in current buffer {:?} ", + req_id, file_handle, offset, data_len_in_buffer, data_in_buffer ); - Ok((handle, req_id, offset, data_len)) + + let write_tracker = PartialWriteRequestTracker::new( + req_id, + &file_handle, + data_len, + remain_data_len, + remain_data_offset, + )?; + + Ok((file_handle, req_id, offset, data_in_buffer, write_tracker)) } } + + /// Used to decode the whole SSHSource as a single BinString + /// + /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. + fn dec_all_as_binstring(&mut self) -> WireResult> { + Ok(BinString(self.take(self.buffer.len())?)) + } } #[derive(Default)] @@ -135,7 +198,7 @@ impl<'g> SftpSink<'g> { /// Finalise the buffer by prepending the payload size and returning /// /// Returns the final index in the buffer as a reference for the space used - pub fn finalise(&mut self) -> usize { + pub fn finalize(&mut self) -> usize { if self.index <= SFTP_FIELD_LEN_LENGTH { warn!("SftpSink trying to terminate it before pushing data"); return 0; @@ -170,9 +233,14 @@ impl<'g> SSHSink for SftpSink<'g> { //#[derive(Debug, Clone)] pub struct SftpHandler<'a> { file_server: &'a mut dyn SftpServer<'a>, + /// buffer_in_len: usize, + /// Once the client and the server have verified the agreed SFTP version the session is initialized initialized: bool, - long_packet: bool, + // /// Use to process SFTP packets that have been received partially and the remaining is expected in successive buffers + // long_packet: bool, + /// Use to process SFTP Write packets that have been received partially and the remaining is expected in successive buffers + partial_write_request_tracker: Option, } impl<'a> SftpHandler<'a> { @@ -188,8 +256,9 @@ impl<'a> SftpHandler<'a> { SftpHandler { file_server, buffer_in_len: buffer_len, - long_packet: false, + // long_packet: false, initialized: false, + partial_write_request_tracker: None, } } @@ -202,70 +271,157 @@ impl<'a> SftpHandler<'a> { ) -> WireResult { let in_len = buffer_in.len(); debug!("Received {:} bytes to process", in_len); - if !self.long_packet & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) { - return Err(WireError::PacketWrong); - } let mut source = SftpSource::new(buffer_in); trace!("Source content: {:?}", source); + let mut sink = SftpSink::new(buffer_out); + + if let Some(mut write_tracker) = self.partial_write_request_tracker.take() { + trace!( + "Processing successive chunks of a long write packet . Stored data: {:?}", + write_tracker + ); + if in_len > write_tracker.remain_data_len as usize { + error!( + "There is too much data in the buffer! {:?} > than max expected {:?}", + in_len, write_tracker.remain_data_len + ); + return Err(WireError::PacketWrong); // TODO: Handle this error instead of failing + } + + let current_write_offset = write_tracker.remain_data_offset; + let data_in_buffer = source.dec_all_as_binstring()?; + + // TODO: Do proper casting with checks u32::try_from(data_in_buffer.0.len()) + let data_in_buffer_len = data_in_buffer.0.len() as u32; + + write_tracker.remain_data_offset += data_in_buffer_len as u64; + write_tracker.remain_data_len -= data_in_buffer_len; + + let file_handle = write_tracker.get_file_handle(); + debug!( + "Processing successive chunks of a long write packet. Writing : file_handle = {:?}, write_offset = {:?}, data = {:?}, data remaining = {:?}", + file_handle, + current_write_offset, + data_in_buffer, + write_tracker.remain_data_len + ); + match self.file_server.write( + &file_handle, + current_write_offset, + data_in_buffer.as_ref(), + ) { + Ok(_) => { + if write_tracker.remain_data_len > 0 { + self.partial_write_request_tracker = Some(write_tracker); + } else { + push_ok(write_tracker.req_id, &mut sink)?; + info!("Finished multi part Write Request"); + self.partial_write_request_tracker = None; // redundant + } + } + Err(e) => { + self.partial_write_request_tracker = None; + error!("SFTP write thrown: {:?}", e); + push_general_failure( + write_tracker.req_id, + "error writing", + &mut sink, + )?; + } + }; + + return Ok(sink.finalize()); + } + + if self.partial_write_request_tracker.is_none() + & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) + { + return Err(WireError::PacketWrong); + } + let packet_length = u32::dec(&mut source)?; trace!("Packet field length content: {}", packet_length); - let mut sink = SftpSink::new(buffer_out); - match SftpPacket::decode_request(&mut source) { Ok(request) => { info!("received request: {:?}", request); self.process_known_request(&mut sink, request).await?; } - Err(e) => match e { - WireError::RanOut => { - warn!( - "RanOut for the SFTP Packet in the source buffer: {:?}", - e - ); - source.rewind(); // Not strictly required - let packet_total_length = source.peak_packet_len()?; - let packet_type = source.peak_packet_type()?; - match packet_type { - SftpNum::SSH_FXP_WRITE => { - self.long_packet = true; - let (file_handle, req_id, offset, data_len) = - source.peak_write_handle_offset_n_data_len()?; - warn!( - "We got a long Write packet. Excellent! total len = {:?}, type = {:?}, req_id = {:?}, handle = {:?}, offset = {:?}, data_len = {:?}", - packet_total_length, - packet_type, - req_id, - file_handle, - offset, - data_len - ); - } - _ => { - error!( - "We do not know how to handle this long packet: {:?}", - packet_type - ); - todo!( - " Push a general failure with the request ID and RanOut comment" - ); - } - }; - } - WireError::UnknownPacket { number: _ } => { - warn!("Error decoding SFTP Packet:{:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; - } - _ => { - error!("Error decoding SFTP Packet: {:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; + Err(e) => { + match e { + WireError::RanOut => { + warn!( + "RanOut for the SFTP Packet in the source buffer: {:?}", + e + ); + // let packet_total_length = source.peak_packet_len()?; + let packet_type = source.peak_packet_type()?; + match packet_type { + SftpNum::SSH_FXP_WRITE => { + let ( + file_handle, + req_id, + offset, + data_in_buffer, + write_tracker, + ) = source + .get_packet_partial_write_content_and_tracker( + )?; + debug!( + "Packet is too long for the source buffer, will write what we have now and continue writing later" + ); + trace!( + "handle = {:?}, req_id = {:?}, offset = {:?}, data_in_buffer = {:?}, write_tracker = {:?}", + file_handle, + req_id, + offset, + data_in_buffer, + write_tracker + ); + + match self.file_server.write( + &file_handle, + offset, + data_in_buffer.as_ref(), + ) { + Ok(_) => { + self.partial_write_request_tracker = + Some(write_tracker); + } + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure( + req_id, + "error writing ", + &mut sink, + )?; + } + }; + } + _ => { + push_general_failure( + ReqId(u32::MAX), + "Unsupported Request: Too long", + &mut sink, + ); + } + }; + } + WireError::UnknownPacket { number: _ } => { + warn!("Error decoding SFTP Packet:{:?}", e); + push_unsupported(ReqId(u32::MAX), &mut sink)?; + } + _ => { + error!("Error decoding SFTP Packet: {:?}", e); + push_unsupported(ReqId(u32::MAX), &mut sink)?; + } } - }, + } }; - Ok(sink.finalise()) + Ok(sink.finalize()) } async fn process_known_request( diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 4c7f1fbf..0acbfc85 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -2,6 +2,12 @@ use crate::proto::{Attrs, FileHandle, Name, StatusCode}; use core::marker::PhantomData; +// TODO: enforce it and do checks for the FileHandle so it properly obscure the file path and user. +// Hint: In stateful server this can be done with a hash function and a dictionary +/// Used during storage of file handle data for long SFTP Write requests +/// Must be observed by SftpServer handle implementations +pub const FILE_HANDLE_MAX_LEN: usize = 256; + pub type SftpOpResult = core::result::Result; /// All trait functions are optional in the SFTP protocol. From e0daffa2cf78adf056a5c4cfe3e82a5fd8d26683 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 17 Sep 2025 14:47:17 +1000 Subject: [PATCH 284/393] Adding basic obscuring of handler This forces the SftpServer to use obscured handles that are size constrain. It also introduces the handle manager, that is a helper that the SFTP server implementer can use for dealing with the obscured handle and a private handle, that can contain all the information that the local server requires. --- demo/sftp/std/src/demosftpserver.rs | 130 ++++++----- .../sftp/std/testing/test_only_file_upload.sh | 2 +- sftp/src/lib.rs | 3 + sftp/src/obscured_file_handle.rs | 219 ++++++++++++++++++ sftp/src/sftphandle.rs | 64 ++--- sftp/src/sftpserver.rs | 33 ++- 6 files changed, 349 insertions(+), 102 deletions(-) create mode 100644 sftp/src/obscured_file_handle.rs diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index f1ad124e..737c7766 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -1,22 +1,32 @@ -use sunset::sshwire::BinString; use sunset_sftp::{ - Attrs, DirReply, FileHandle, Filename, Name, NameEntry, ReadReply, SftpOpResult, - SftpServer, StatusCode, + Attrs, DirReply, Filename, HandleManager, Name, NameEntry, ObscuredFileHandle, + PathFinder, ReadReply, SftpOpResult, SftpServer, StatusCode, }; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; +struct PrivateFileHandler { + file_path: String, + permissions: Option, +} + +impl PathFinder for PrivateFileHandler { + fn matches_path(&self, path: &str) -> bool { + self.file_path.as_str().eq_ignore_ascii_case(path) + } +} + pub struct DemoSftpServer { - valid_handlers: Vec, // TODO: Obscure the handlers user_path: String, + handlers_manager: HandleManager, } impl DemoSftpServer { pub fn new(user: String) -> Self { DemoSftpServer { - valid_handlers: vec![], user_path: format!("/{}/", user.clone()), + handlers_manager: HandleManager::new(), } } } @@ -25,18 +35,24 @@ impl SftpServer<'_> for DemoSftpServer { fn open( &mut self, filename: &str, - _attrs: &Attrs, - ) -> SftpOpResult> { - if self.valid_handlers.contains(&filename.to_string()) { + attrs: &Attrs, + ) -> SftpOpResult { + debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); + + if self.handlers_manager.is_open(filename) { warn!("File {:?} already open, won't allow it", filename); return Err(StatusCode::SSH_FX_PERMISSION_DENIED); } - self.valid_handlers.push(filename.to_string()); + let fh = self.handlers_manager.create_handle(PrivateFileHandler { + file_path: filename.into(), + permissions: attrs.permissions, + }); + warn!( + "Filename \"{:?}\" will have the obscured file handle: {:?}", + filename, fh + ); - let fh = FileHandle(BinString( - self.valid_handlers.last().expect("just pushed an element").as_bytes(), - )); Ok(fh) } @@ -57,73 +73,83 @@ impl SftpServer<'_> for DemoSftpServer { }])) } - fn close(&mut self, handle: &FileHandle) -> SftpOpResult<()> { - let initial_count = self.valid_handlers.len(); - if initial_count == 0 { - log::error!( - "SftpServer Close operation with no handles stored: handle = {:?}", - handle + fn close( + &mut self, + obscure_file_handle: &ObscuredFileHandle, + ) -> SftpOpResult<()> { + if let Some(handle) = + self.handlers_manager.remove_handle(obscure_file_handle) + { + debug!( + "SftpServer Close operation on {:?} was successful", + handle.file_path + ); + Ok(()) + } else { + error!( + "SftpServer Close operation on handle {:?} failed", + obscure_file_handle ); - return Err(StatusCode::SSH_FX_FAILURE); + Err(StatusCode::SSH_FX_FAILURE) } + } - let filename = - String::from_utf8(handle.0.as_ref().to_vec()).unwrap_or("".into()); + fn write( + &mut self, + obscured_file_handle: &ObscuredFileHandle, + offset: u64, + buf: &[u8], + ) -> SftpOpResult<()> { + let private_file_handle = self + .handlers_manager + .get_handle_value_as_ref(obscured_file_handle) + .ok_or(StatusCode::SSH_FX_FAILURE)?; - if !self.valid_handlers.contains(&filename) { - log::error!( - "SftpServer Close operation could not match an stored handler: handle = {:?}", - handle - ); - return Err(StatusCode::SSH_FX_FAILURE); - } - self.valid_handlers.retain(|handler| handler.ne(&filename)); - log::debug!("SftpServer Close operation on {:?} was successful", filename); + let permissions_poxit = (private_file_handle + .permissions + .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED))?; + + if (permissions_poxit & 0o222) == 0 { + return Err(StatusCode::SSH_FX_PERMISSION_DENIED); + }; + + log::debug!( + "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buf = {:?}", + obscured_file_handle, + private_file_handle.file_path, + offset, + String::from_utf8(buf.to_vec()) + ); Ok(()) } fn read( &mut self, - handle: &FileHandle, + obscured_file_handle: &ObscuredFileHandle, offset: u64, - reply: &mut ReadReply<'_, '_>, + _reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", - handle, + obscured_file_handle, offset ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - fn write( - &mut self, - handle: &FileHandle, - offset: u64, - buf: &[u8], - ) -> SftpOpResult<()> { - log::debug!( - "SftpServer Write operation: handle = {:?}, offset = {:?}, buf = {:?}", - handle, - offset, - String::from_utf8(buf.to_vec()) - ); - Ok(()) - } - - fn opendir(&mut self, dir: &str) -> SftpOpResult> { + fn opendir(&mut self, dir: &str) -> SftpOpResult { log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } fn readdir( &mut self, - handle: &FileHandle, - reply: &mut DirReply<'_, '_>, + obscured_file_handle: &ObscuredFileHandle, + _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", - handle + obscured_file_handle ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } diff --git a/demo/sftp/std/testing/test_only_file_upload.sh b/demo/sftp/std/testing/test_only_file_upload.sh index 97f93269..92d34e71 100755 --- a/demo/sftp/std/testing/test_only_file_upload.sh +++ b/demo/sftp/std/testing/test_only_file_upload.sh @@ -26,9 +26,9 @@ echo "=============" # put ./4kB_random # put ./16kB_random # put ./1MB_random +# put ./MaxMaybe_random sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF put ./64kB_random -put ./MaxMaybe_random bye EOF diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 5e7d91ba..16a8ba76 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,3 +1,4 @@ +mod obscured_file_handle; mod proto; mod sftphandle; mod sftpserver; @@ -9,6 +10,8 @@ pub use sftpserver::SftpServer; pub use sftphandle::SftpHandler; +pub use obscured_file_handle::{HandleManager, ObscuredFileHandle, PathFinder}; + pub use proto::Attrs; pub use proto::FileHandle; pub use proto::Filename; diff --git a/sftp/src/obscured_file_handle.rs b/sftp/src/obscured_file_handle.rs new file mode 100644 index 00000000..34a678d4 --- /dev/null +++ b/sftp/src/obscured_file_handle.rs @@ -0,0 +1,219 @@ +use crate::FileHandle; + +use sunset::sshwire::{BinString, WireError}; + +use std::collections::HashMap; + +pub const FILE_HANDLE_MAX_LEN: usize = 8; + +/// Obscured file handle using Linear Congruential Generator (LCG) for pseudo-random generation. +/// +/// This struct provides a fixed-length handle that appears random but is deterministic +/// based on the input seed. Uses LCG with constants from Numerical Recipes. +/// +/// # Limitations +/// +/// - **Not cryptographically secure**: Predictable if the algorithm and seed are known +/// - **Limited entropy**: u8 seed provides only 256 different possible handles +/// - **Linear correlations**: Sequential seeds produce statistically correlated outputs +/// - **Reversible**: Given the handle, the original seed can potentially be recovered +/// - **Not suitable for security**: Should only be used for obscuration, not protection +/// +/// # Security Note +/// +/// This is intended for basic handle obscuration in SFTP to prevent casual observation +/// of handle-to-file mappings. It is NOT suitable for cryptographic purposes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ObscuredFileHandle { + data: [u8; FILE_HANDLE_MAX_LEN], +} + +impl ObscuredFileHandle { + /// Generate a pseudo-random handle from a u8 seed using Linear Congruential Generator. + /// + /// Same seed will always produce the same handle. Only 256 different handles + /// are possible due to u8 seed limitation. This is deliberate to imply its limitations + /// + /// TODO: If this library is to be hardened this is a point to address + pub fn new(seed: u8) -> Self { + let mut data = [0u8; FILE_HANDLE_MAX_LEN]; + + // Simple Linear Congruential Generator (LCG) + // Using constants from Numerical Recipes + let mut state = seed as u64; + + for chunk in data.chunks_mut(8) { + // LCG: next = (a * current + c) mod 2^64 + state = state.wrapping_mul(1664525).wrapping_add(1013904223); + + let bytes = state.to_le_bytes(); + let copy_len = chunk.len().min(8); + chunk[..copy_len].copy_from_slice(&bytes[..copy_len]); + } + + Self { data } + } + + /// Get the handle as a byte slice + pub fn as_bytes(&self) -> &[u8] { + &self.data + } + + /// Get the handle as a Vec for wire protocol + pub fn to_vec(&self) -> Vec { + self.data.to_vec() + } + + /// Create from existing bytes (for parsing from wire) + pub fn from_bytes(bytes: &[u8]) -> Option { + if bytes.len() != FILE_HANDLE_MAX_LEN { + return None; + } + + let mut data = [0u8; FILE_HANDLE_MAX_LEN]; + data.copy_from_slice(bytes); + Some(Self { data }) + } + + /// Create from BinString (for parsing from SFTP wire protocol) + pub fn from_binstring(binstring: &BinString<'_>) -> Option { + Self::from_bytes(binstring.0) + } + + /// Create from FileHandle (for parsing from SFTP wire protocol) + pub fn from_filehandle(file_handle: &FileHandle<'_>) -> Option { + Self::from_bytes(file_handle.0.0) + } + + /// Convert to BinString for SFTP wire protocol + pub fn to_binstring(&self) -> BinString<'_> { + BinString(&self.data) + } + + /// Convert to FileHandle for SFTP wire protocol + pub fn to_filehandle(&self) -> FileHandle<'_> { + FileHandle(self.to_binstring()) + } +} + +// Display trait for debugging/logging +impl core::fmt::Display for ObscuredFileHandle { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + for byte in &self.data { + write!(f, "{:02x}", byte)?; + } + Ok(()) + } +} + +// Standard trait implementations for BinString conversion +impl<'a> From<&'a ObscuredFileHandle> for BinString<'a> { + fn from(handle: &'a ObscuredFileHandle) -> Self { + handle.to_binstring() + } +} + +impl<'a> TryFrom<&BinString<'a>> for ObscuredFileHandle { + type Error = WireError; + + fn try_from(binstring: &BinString<'a>) -> Result { + Self::from_binstring(binstring).ok_or(WireError::BadString) + } +} + +impl<'a> TryFrom> for ObscuredFileHandle { + type Error = WireError; + + fn try_from(binstring: BinString<'a>) -> Result { + Self::try_from(&binstring) + } +} + +// Conversions with proto::FileHandle +impl<'a> From<&'a ObscuredFileHandle> for crate::proto::FileHandle<'a> { + fn from(handle: &'a ObscuredFileHandle) -> Self { + crate::proto::FileHandle(handle.into()) + } +} + +impl<'a> TryFrom<&crate::proto::FileHandle<'a>> for ObscuredFileHandle { + type Error = WireError; + + fn try_from( + file_handle: &crate::proto::FileHandle<'a>, + ) -> Result { + Self::try_from(&file_handle.0) + } +} + +impl<'a> TryFrom> for ObscuredFileHandle { + type Error = WireError; + + fn try_from( + file_handle: crate::proto::FileHandle<'a>, + ) -> Result { + Self::try_from(&file_handle) + } +} + +/// Used to standarise finding a path within the HandleManager +pub trait PathFinder { + /// Helper function to find elements stored in the HandleManager that matches the give path + fn matches_path(&self, path: &str) -> bool; +} + +// Example usage structure for managing handles +pub struct HandleManager +where + T: PathFinder, +{ + next_handle_id: u8, + handle_map: HashMap, +} + +impl HandleManager { + pub fn new() -> Self { + Self { next_handle_id: 1, handle_map: HashMap::new() } + } + + pub fn create_handle(&mut self, value: T) -> ObscuredFileHandle { + let handle = ObscuredFileHandle::new(self.next_handle_id); + self.next_handle_id += 1; + + self.handle_map.insert(handle, value); + handle + } + + pub fn get_handle_value_as_ref( + &self, + handle: &ObscuredFileHandle, + ) -> Option<&T> { + self.handle_map.get(handle) + } + + pub fn remove_handle(&mut self, handle: &ObscuredFileHandle) -> Option { + self.handle_map.remove(handle) + } + + pub fn handle_exists(&self, handle: &ObscuredFileHandle) -> bool { + self.handle_map.contains_key(handle) + } + + pub fn is_open(&self, filename: &str) -> bool { + if self.handle_map.is_empty() { + return false; + } + self.handle_map.iter().any(|(_, element)| element.matches_path(filename)) + // TODO: Fix this. We need to be able to find out if the filename has been open. That cannot be done with a general T. Is will need to implement a trait to check that + // true + } +} + +impl Default for HandleManager +where + T: PathFinder, +{ + fn default() -> Self { + Self::new() + } +} diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 42ce44ec..2315c59a 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,10 +1,10 @@ -use crate::FileHandle; use crate::proto::{ self, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SFTP_WRITE_REQID_INDEX, SftpNum, SftpPacket, Status, StatusCode, }; -use crate::sftpserver::{FILE_HANDLE_MAX_LEN, SftpServer}; +use crate::sftpserver::SftpServer; +use crate::{FileHandle, ObscuredFileHandle}; use sunset::sshwire::{ BinString, SSHDecode, SSHSink, SSHSource, WireError, WireResult, @@ -19,8 +19,7 @@ use std::usize; #[derive(Debug)] struct PartialWriteRequestTracker { req_id: ReqId, - file_handle: [u8; FILE_HANDLE_MAX_LEN], // TODO: Change the file handle in SftpServer functions signature so it has a sort fixed length handle. - file_handle_len: usize, + obscure_file_handle: ObscuredFileHandle, // TODO: Change the file handle in SftpServer functions signature so it has a sort fixed length handle. data_len: u32, remain_data_len: u32, remain_data_offset: u64, @@ -29,31 +28,23 @@ struct PartialWriteRequestTracker { impl PartialWriteRequestTracker { pub fn new( req_id: ReqId, - file_handle: &FileHandle<'_>, + obscure_file_handle: ObscuredFileHandle, data_len: u32, remain_data_len: u32, remain_data_offset: u64, ) -> WireResult { - let handle_data = file_handle.0.as_ref(); - if handle_data.len() > FILE_HANDLE_MAX_LEN { - return Err(WireError::PacketWrong); // Handle too large - } let mut ret = PartialWriteRequestTracker { req_id, - file_handle: [0u8; FILE_HANDLE_MAX_LEN], - file_handle_len: handle_data.len(), + obscure_file_handle: obscure_file_handle, data_len, remain_data_len, remain_data_offset, }; - ret.file_handle[..ret.file_handle_len] - .copy_from_slice(file_handle.0.as_ref()); - Ok(ret) } - pub fn get_file_handle(&self) -> FileHandle<'_> { - FileHandle(BinString(&self.file_handle[..self.file_handle_len])) + pub fn get_file_handle(&self) -> ObscuredFileHandle { + self.obscure_file_handle.clone() } } @@ -136,7 +127,7 @@ impl<'de> SftpSource<'de> { fn get_packet_partial_write_content_and_tracker( &mut self, ) -> WireResult<( - FileHandle<'de>, + ObscuredFileHandle, ReqId, u64, BinString<'de>, @@ -149,6 +140,11 @@ impl<'de> SftpSource<'de> { self.index = SFTP_WRITE_REQID_INDEX; let req_id = ReqId::dec(self)?; let file_handle = FileHandle::dec(self)?; + + let obscured_file_handle = + ObscuredFileHandle::from_binstring(&file_handle.0) + .ok_or(WireError::BadString)?; + let offset = u64::dec(self)?; let data_len = u32::dec(self)?; @@ -166,13 +162,14 @@ impl<'de> SftpSource<'de> { let write_tracker = PartialWriteRequestTracker::new( req_id, - &file_handle, + ObscuredFileHandle::from_filehandle(&file_handle) + .ok_or(WireError::BadString)?, data_len, remain_data_len, remain_data_offset, )?; - Ok((file_handle, req_id, offset, data_in_buffer, write_tracker)) + Ok((obscured_file_handle, req_id, offset, data_in_buffer, write_tracker)) } } @@ -283,11 +280,12 @@ impl<'a> SftpHandler<'a> { write_tracker ); if in_len > write_tracker.remain_data_len as usize { + // TODO: Investigate if we are receiving one packet and the beginning of the next one error!( "There is too much data in the buffer! {:?} > than max expected {:?}", in_len, write_tracker.remain_data_len ); - return Err(WireError::PacketWrong); // TODO: Handle this error instead of failing + return Err(WireError::PacketWrong); // TODO: Handle this error instead of failing. } let current_write_offset = write_tracker.remain_data_offset; @@ -299,16 +297,16 @@ impl<'a> SftpHandler<'a> { write_tracker.remain_data_offset += data_in_buffer_len as u64; write_tracker.remain_data_len -= data_in_buffer_len; - let file_handle = write_tracker.get_file_handle(); + let obscure_file_handle = write_tracker.get_file_handle(); debug!( - "Processing successive chunks of a long write packet. Writing : file_handle = {:?}, write_offset = {:?}, data = {:?}, data remaining = {:?}", - file_handle, + "Processing successive chunks of a long write packet. Writing : obscure_file_handle = {:?}, write_offset = {:?}, data = {:?}, data remaining = {:?}", + obscure_file_handle, current_write_offset, data_in_buffer, write_tracker.remain_data_len ); match self.file_server.write( - &file_handle, + &obscure_file_handle, current_write_offset, data_in_buffer.as_ref(), ) { @@ -374,7 +372,7 @@ impl<'a> SftpHandler<'a> { ); trace!( "handle = {:?}, req_id = {:?}, offset = {:?}, data_in_buffer = {:?}, write_tracker = {:?}", - file_handle, + file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle req_id, offset, data_in_buffer, @@ -405,7 +403,7 @@ impl<'a> SftpHandler<'a> { ReqId(u32::MAX), "Unsupported Request: Too long", &mut sink, - ); + )?; } }; } @@ -460,10 +458,12 @@ impl<'a> SftpHandler<'a> { } SftpPacket::Open(req_id, open) => { match self.file_server.open(open.filename.as_str()?, &open.attrs) { - Ok(handle) => { + Ok(obscured_file_handle) => { let response = SftpPacket::Handle( req_id, - proto::Handle { handle: handle.into() }, + proto::Handle { + handle: obscured_file_handle.to_filehandle(), + }, ); response.encode_response(sink)?; info!("Sending '{:?}'", response); @@ -476,7 +476,8 @@ impl<'a> SftpHandler<'a> { } SftpPacket::Write(req_id, write) => { match self.file_server.write( - &write.handle, + &ObscuredFileHandle::from_filehandle(&write.handle) + .ok_or(WireError::BadString)?, write.offset, write.data.as_ref(), ) { @@ -488,7 +489,10 @@ impl<'a> SftpHandler<'a> { }; } SftpPacket::Close(req_id, close) => { - match self.file_server.close(&close.handle) { + match self.file_server.close( + &ObscuredFileHandle::from_filehandle(&close.handle) + .ok_or(WireError::BadString)?, + ) { Ok(_) => push_ok(req_id, sink)?, Err(e) => { error!("SFTP Close thrown: {:?}", e); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 0acbfc85..2ac34efc 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,13 +1,10 @@ -use crate::proto::{Attrs, FileHandle, Name, StatusCode}; +use crate::{ + ObscuredFileHandle, + proto::{Attrs, Name, StatusCode}, +}; use core::marker::PhantomData; -// TODO: enforce it and do checks for the FileHandle so it properly obscure the file path and user. -// Hint: In stateful server this can be done with a hash function and a dictionary -/// Used during storage of file handle data for long SFTP Write requests -/// Must be observed by SftpServer handle implementations -pub const FILE_HANDLE_MAX_LEN: usize = 256; - pub type SftpOpResult = core::result::Result; /// All trait functions are optional in the SFTP protocol. @@ -15,14 +12,12 @@ pub type SftpOpResult = core::result::Result; /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. pub trait SftpServer<'a> { - // type Handle<'a>: Into> + TryFrom> + Debug + Copy; - /// Opens a file or directory for reading/writing fn open( - &mut self, + &'_ mut self, filename: &str, attrs: &Attrs, - ) -> SftpOpResult> { + ) -> SftpOpResult { log::error!( "SftpServer Open operation not defined: filename = {:?}, attrs = {:?}", filename, @@ -32,7 +27,7 @@ pub trait SftpServer<'a> { } /// Close either a file or directory handle - fn close(&mut self, handle: &FileHandle) -> SftpOpResult<()> { + fn close(&mut self, handle: &ObscuredFileHandle) -> SftpOpResult<()> { log::error!("SftpServer Close operation not defined: handle = {:?}", handle); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -40,13 +35,13 @@ pub trait SftpServer<'a> { fn read( &mut self, - handle: &FileHandle, + obscured_file_handle: &ObscuredFileHandle, offset: u64, reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", - handle, + obscured_file_handle, offset ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -54,32 +49,32 @@ pub trait SftpServer<'a> { fn write( &mut self, - handle: &FileHandle, + obscured_file_handle: &ObscuredFileHandle, offset: u64, buf: &[u8], ) -> SftpOpResult<()> { log::error!( "SftpServer Write operation: handle = {:?}, offset = {:?}, buf = {:?}", - handle, + obscured_file_handle, offset, String::from_utf8(buf.to_vec()) ); Ok(()) } - fn opendir(&mut self, dir: &str) -> SftpOpResult> { + fn opendir(&mut self, dir: &str) -> SftpOpResult { log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } fn readdir( &mut self, - handle: &FileHandle, + obscured_file_handle: &ObscuredFileHandle, reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", - handle + obscured_file_handle ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } From 7474cfb506b44e3ca2d9437eeb36374504060048 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 17 Sep 2025 16:26:06 +1000 Subject: [PATCH 285/393] Minimal SFTP Server implementation of write 64kB write pass the test File sent and written are identical. That is a good start. Next I will find what is wrong passed 64kB --- demo/sftp/std/src/demosftpserver.rs | 53 +++++++++++++++---- demo/sftp/std/src/main.rs | 7 +-- .../sftp/std/testing/test_only_file_upload.sh | 3 +- sftp/src/proto.rs | 6 +-- sftp/src/sftphandle.rs | 50 +++-------------- sftp/src/sftpserver.rs | 10 ++-- 6 files changed, 67 insertions(+), 62 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 737c7766..da291a84 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -5,10 +5,12 @@ use sunset_sftp::{ #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; +use std::{fs::File, os::unix::fs::FileExt}; struct PrivateFileHandler { file_path: String, permissions: Option, + file: File, } impl PathFinder for PrivateFileHandler { @@ -18,18 +20,16 @@ impl PathFinder for PrivateFileHandler { } pub struct DemoSftpServer { - user_path: String, + base_path: String, handlers_manager: HandleManager, } impl DemoSftpServer { - pub fn new(user: String) -> Self { - DemoSftpServer { - user_path: format!("/{}/", user.clone()), - handlers_manager: HandleManager::new(), - } + pub fn new(base_path: String) -> Self { + DemoSftpServer { base_path, handlers_manager: HandleManager::new() } } } + impl SftpServer<'_> for DemoSftpServer { // Mocking an Open operation. Will not check for permissions fn open( @@ -44,11 +44,31 @@ impl SftpServer<'_> for DemoSftpServer { return Err(StatusCode::SSH_FX_PERMISSION_DENIED); } + let poxit_attr = attrs + .permissions + .as_ref() + .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED)?; + let can_write = poxit_attr & 0o222 > 0; + let can_read = poxit_attr & 0o444 > 0; + debug!( + "File open for read/write access: can_read={:?}, can_write={:?}", + can_read, can_write + ); + + let file = File::options() + .read(can_read) + .write(can_write) + .create(true) + .open(filename) + .map_err(|_| StatusCode::SSH_FX_FAILURE)?; + let fh = self.handlers_manager.create_handle(PrivateFileHandler { file_path: filename.into(), permissions: attrs.permissions, + file, }); - warn!( + + debug!( "Filename \"{:?}\" will have the obscured file handle: {:?}", filename, fh ); @@ -59,7 +79,7 @@ impl SftpServer<'_> for DemoSftpServer { fn realpath(&mut self, dir: &str) -> SftpOpResult> { debug!("finding path for: {:?}", dir); Ok(Name(vec![NameEntry { - filename: Filename::from(self.user_path.as_str()), + filename: Filename::from(self.base_path.as_str()), _longname: Filename::from(""), attrs: Attrs { size: None, @@ -84,6 +104,7 @@ impl SftpServer<'_> for DemoSftpServer { "SftpServer Close operation on {:?} was successful", handle.file_path ); + drop(handle.file); // Not really required but illustrative Ok(()) } else { error!( @@ -113,13 +134,27 @@ impl SftpServer<'_> for DemoSftpServer { return Err(StatusCode::SSH_FX_PERMISSION_DENIED); }; - log::debug!( + log::trace!( "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buf = {:?}", obscured_file_handle, private_file_handle.file_path, offset, String::from_utf8(buf.to_vec()) ); + let bytes_written = private_file_handle + .file + .write_at(buf, offset) + .map_err(|_| StatusCode::SSH_FX_FAILURE)?; + + log::debug!( + "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buffer length = {:?}, bytes written = {:?}", + obscured_file_handle, + private_file_handle.file_path, + offset, + buf.len(), + bytes_written + ); + Ok(()) } diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 8d98977c..54aadd00 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -148,10 +148,11 @@ impl DemoServer for StdDemo { match { let mut stdio = serv.stdio(ch).await?; - let mut file_server = DemoSftpServer::new("user".to_string()); + let mut file_server = DemoSftpServer::new( + "./demo/sftp/std/testing/out/".to_string(), + ); - let mut sftp_handler = - SftpHandler::new(&mut file_server, buffer_in.len()); + let mut sftp_handler = SftpHandler::new(&mut file_server); loop { let lr = stdio.read(&mut buffer_in).await?; trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); diff --git a/demo/sftp/std/testing/test_only_file_upload.sh b/demo/sftp/std/testing/test_only_file_upload.sh index 92d34e71..465dabbb 100755 --- a/demo/sftp/std/testing/test_only_file_upload.sh +++ b/demo/sftp/std/testing/test_only_file_upload.sh @@ -32,7 +32,7 @@ put ./64kB_random bye EOF - +diff ./64kB_random ./out/64kB_random if [ $? -eq 0 ]; then echo "PASS" else @@ -41,5 +41,6 @@ fi echo "Cleaning up local files..." rm -f ./*_random +rm -f ./out/*_random echo "Upload test completed." \ No newline at end of file diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 17d21a96..252f1b4b 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -15,7 +15,7 @@ pub const SFTP_MINIMUM_PACKET_LEN: usize = 9; /// SFTP packets have the packet type after a u32 length field pub const SFTP_FIELD_ID_INDEX: usize = 4; /// SFTP packets ID length is 1 byte -pub const SFTP_FIELD_ID_LEN: usize = 1; +// pub const SFTP_FIELD_ID_LEN: usize = 1; /// SFTP packets start with the length field pub const SFTP_FIELD_LEN_INDEX: usize = 0; /// SFTP packets length field us u32 @@ -24,13 +24,13 @@ pub const SFTP_FIELD_LEN_LENGTH: usize = 4; // SSH_FXP_WRITE SFTP Packet definition used to decode long packets that do not fit in one buffer /// SFTP SSH_FXP_WRITE Packet cannot be shorter than this (len:4+pnum:1+rid:4+hand:4+0+data:4+0 bytes = 17 bytes) [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) -pub const SFTP_MINIMUM_WRITE_PACKET_LEN: usize = 17; +// pub const SFTP_MINIMUM_WRITE_PACKET_LEN: usize = 17; /// SFTP SSH_FXP_WRITE Packet request id field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) pub const SFTP_WRITE_REQID_INDEX: usize = 5; /// SFTP SSH_FXP_WRITE Packet handle field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) -pub const SFTP_WRITE_HANDLE_INDEX: usize = 9; +// pub const SFTP_WRITE_HANDLE_INDEX: usize = 9; // TODO is utf8 enough, or does this need to be an opaque binstring? #[derive(Debug, SSHEncode, SSHDecode)] diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 2315c59a..e7cdba85 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -20,7 +20,6 @@ use std::usize; struct PartialWriteRequestTracker { req_id: ReqId, obscure_file_handle: ObscuredFileHandle, // TODO: Change the file handle in SftpServer functions signature so it has a sort fixed length handle. - data_len: u32, remain_data_len: u32, remain_data_offset: u64, } @@ -29,18 +28,15 @@ impl PartialWriteRequestTracker { pub fn new( req_id: ReqId, obscure_file_handle: ObscuredFileHandle, - data_len: u32, remain_data_len: u32, remain_data_offset: u64, ) -> WireResult { - let mut ret = PartialWriteRequestTracker { + Ok(PartialWriteRequestTracker { req_id, obscure_file_handle: obscure_file_handle, - data_len, remain_data_len, remain_data_offset, - }; - Ok(ret) + }) } pub fn get_file_handle(&self) -> ObscuredFileHandle { @@ -100,25 +96,6 @@ impl<'de> SftpSource<'de> { } } - /// Peaks the buffer for packet length. This does not advance the reading index - /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail - /// - /// **Warning**: will only work in well formed packets, in other case the result will contain garbage - fn peak_packet_len(&self) -> WireResult { - if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - Err(WireError::PacketWrong) - } else { - let mut raw_bytes = [0u8; 4]; - raw_bytes.copy_from_slice( - &self.buffer[SFTP_FIELD_LEN_INDEX - ..SFTP_FIELD_LEN_INDEX + SFTP_FIELD_LEN_LENGTH], - ); - - Ok(u32::from_be_bytes(raw_bytes)) - } - } - /// Assuming that the buffer contains a Write request packet initial bytes, Peaks the buffer for the handle length. This does not advance the reading index /// /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail @@ -164,7 +141,6 @@ impl<'de> SftpSource<'de> { req_id, ObscuredFileHandle::from_filehandle(&file_handle) .ok_or(WireError::BadString)?, - data_len, remain_data_len, remain_data_offset, )?; @@ -229,38 +205,28 @@ impl<'g> SSHSink for SftpSink<'g> { //#[derive(Debug, Clone)] pub struct SftpHandler<'a> { + /// The local SFTP File server implementing the basic SFTP requests defined by `SftpServer` file_server: &'a mut dyn SftpServer<'a>, - /// - buffer_in_len: usize, + /// Once the client and the server have verified the agreed SFTP version the session is initialized initialized: bool, - // /// Use to process SFTP packets that have been received partially and the remaining is expected in successive buffers - // long_packet: bool, + /// Use to process SFTP Write packets that have been received partially and the remaining is expected in successive buffers partial_write_request_tracker: Option, } impl<'a> SftpHandler<'a> { - pub fn new( - file_server: &'a mut impl SftpServer<'a>, - buffer_len: usize, // max_file_handlers: u32 - ) -> Self { - if buffer_len < 256 { - warn!( - "Buffer length too small, must be at least 256 bytes. You are in uncharted territory" - ) - } + pub fn new(file_server: &'a mut impl SftpServer<'a>) -> Self { SftpHandler { file_server, - buffer_in_len: buffer_len, - // long_packet: false, + initialized: false, partial_write_request_tracker: None, } } /// Decodes the buffer_in request, process the request delegating operations to an Struct implementing SftpServer, - /// serialises an answer in buffer_out and return the length usedd in buffer_out + /// serializes an answer in buffer_out and return the length used in buffer_out pub async fn process( &mut self, buffer_in: &[u8], diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 2ac34efc..37a51949 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -37,7 +37,7 @@ pub trait SftpServer<'a> { &mut self, obscured_file_handle: &ObscuredFileHandle, offset: u64, - reply: &mut ReadReply<'_, '_>, + _reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", @@ -70,7 +70,7 @@ pub trait SftpServer<'a> { fn readdir( &mut self, obscured_file_handle: &ObscuredFileHandle, - reply: &mut DirReply<'_, '_>, + _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", @@ -86,20 +86,22 @@ pub trait SftpServer<'a> { } } +// TODO: Define this pub struct ReadReply<'g, 'a> { chan: ChanOut<'g, 'a>, } impl<'g, 'a> ReadReply<'g, 'a> { - pub fn reply(self, data: &[u8]) {} + pub fn reply(self, _data: &[u8]) {} } +// TODO: Define this pub struct DirReply<'g, 'a> { chan: ChanOut<'g, 'a>, } impl<'g, 'a> DirReply<'g, 'a> { - pub fn reply(self, data: &[u8]) {} + pub fn reply(self, _data: &[u8]) {} } // TODO: Implement correct Channel Out From 6a938c278815b91614cbc9df02fbc393300ffd0c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 17 Sep 2025 19:51:00 +1000 Subject: [PATCH 286/393] Troubleshooting the 65kB limit: There are two write requests I was assuming that the client would wait for a `SSH_FXP_STATUS` Ok, but it does not. I need to handle a second request segment in the same SftpSource --- demo/sftp/std/testing/test_limit_write_req.sh | 33 ++++++++++ .../sftp/std/testing/test_only_file_upload.sh | 46 ------------- demo/sftp/std/testing/test_write_requests.sh | 40 ++++++++++++ sftp/src/sftperror.rs | 17 +++++ sftp/src/sftphandle.rs | 64 ++++++++++++++----- 5 files changed, 137 insertions(+), 63 deletions(-) create mode 100755 demo/sftp/std/testing/test_limit_write_req.sh delete mode 100755 demo/sftp/std/testing/test_only_file_upload.sh create mode 100755 demo/sftp/std/testing/test_write_requests.sh create mode 100644 sftp/src/sftperror.rs diff --git a/demo/sftp/std/testing/test_limit_write_req.sh b/demo/sftp/std/testing/test_limit_write_req.sh new file mode 100755 index 00000000..45614183 --- /dev/null +++ b/demo/sftp/std/testing/test_limit_write_req.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Generate random data files +echo "Generating random data files..." + +dd if=/dev/random bs=1024 count=65 of=./TwoWriteRequests_random 2>/dev/null # Fails + + +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." +echo "Test Results:" +echo "=============" + +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +put ./TwoWriteRequests_random +bye +EOF + +diff ./TwoWriteRequests_random ./out/TwoWriteRequests_random +if [ $? -eq 0 ]; then + echo "PASS" +else + echo "FAIL" +fi + +echo "Cleaning up local files..." +rm -f ./*_random +rm -f ./out/*_random + +echo "Upload test completed." \ No newline at end of file diff --git a/demo/sftp/std/testing/test_only_file_upload.sh b/demo/sftp/std/testing/test_only_file_upload.sh deleted file mode 100755 index 465dabbb..00000000 --- a/demo/sftp/std/testing/test_only_file_upload.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -# Set remote server details -REMOTE_HOST="192.168.69.2" -REMOTE_USER="any" - -# Generate random data files -echo "Generating random data files..." -dd if=/dev/random bs=128 count=1 of=./128B_random 2>/dev/null -dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null -dd if=/dev/random bs=1024 count=2 of=./2kB_random 2>/dev/null -dd if=/dev/random bs=1024 count=4 of=./4kB_random 2>/dev/null -dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null -dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null # OK -dd if=/dev/random bs=1024 count=65 of=./MaxMaybe_random 2>/dev/null # Fails -dd if=/dev/random bs=1024 count=1024 of=./1MB_random 2>/dev/null - - -echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." -echo "Test Results:" -echo "=============" - -# put ./128B_random -# put ./512B_random -# put ./2kB_random -# put ./4kB_random -# put ./16kB_random -# put ./1MB_random -# put ./MaxMaybe_random -sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF -put ./64kB_random -bye -EOF - -diff ./64kB_random ./out/64kB_random -if [ $? -eq 0 ]; then - echo "PASS" -else - echo "FAIL" -fi - -echo "Cleaning up local files..." -rm -f ./*_random -rm -f ./out/*_random - -echo "Upload test completed." \ No newline at end of file diff --git a/demo/sftp/std/testing/test_write_requests.sh b/demo/sftp/std/testing/test_write_requests.sh new file mode 100755 index 00000000..dc615c1a --- /dev/null +++ b/demo/sftp/std/testing/test_write_requests.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Define test files +FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random") + +# Generate random data files +echo "Generating random data files..." +dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null +dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=65 of=./65kB_random 2>/dev/null + +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." + +# Upload all files +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +$(printf 'put ./%s\n' "${FILES[@]}") +bye +EOF + +echo "Test Results:" +echo "=============" + +# Test each file +for file in "${FILES[@]}"; do + if diff "./${file}" "./out/${file}" >/dev/null 2>&1; then + echo "PASS: ${file}" + else + echo "FAIL: ${file}" + fi +done + +echo "Cleaning up local files..." +rm -f ./*_random ./out/*_random + +echo "Upload test completed." \ No newline at end of file diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs new file mode 100644 index 00000000..b78393bb --- /dev/null +++ b/sftp/src/sftperror.rs @@ -0,0 +1,17 @@ +use core::convert::From; + +use sunset::sshwire::WireError; + +#[derive(Debug)] +pub enum SftpError { + WireError(WireError), + // SshError(SshError), +} + +impl From for SftpError { + fn from(value: WireError) -> Self { + SftpError::WireError(value) + } +} + +pub type SftpResult = Result; diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index e7cdba85..08cc031a 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,7 +1,7 @@ use crate::proto::{ - self, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, - SFTP_FIELD_LEN_LENGTH, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, - SFTP_WRITE_REQID_INDEX, SftpNum, SftpPacket, Status, StatusCode, + self, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_LENGTH, + SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SFTP_WRITE_REQID_INDEX, SftpNum, + SftpPacket, Status, StatusCode, }; use crate::sftpserver::SftpServer; use crate::{FileHandle, ObscuredFileHandle}; @@ -96,6 +96,24 @@ impl<'de> SftpSource<'de> { } } + /// Peaks the buffer for packet type adding an offset. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: This might only work in special conditions, such as those where the , in other case the result will contain garbage + fn peak_packet_type_with_offset( + &self, + starting_offset: usize, + ) -> WireResult { + // const SFTP_ID_BUFFER_INDEX: usize = 4; // All SFTP packet have the packet type after a u32 length field + // const SFTP_MINIMUM_LENGTH: usize = 9; // Corresponds to a minimal SSH_FXP_INIT packet + if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + Err(WireError::PacketWrong) + } else { + Ok(SftpNum::from(self.buffer[starting_offset + SFTP_FIELD_ID_INDEX])) + } + } + /// Assuming that the buffer contains a Write request packet initial bytes, Peaks the buffer for the handle length. This does not advance the reading index /// /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail @@ -149,12 +167,19 @@ impl<'de> SftpSource<'de> { } } - /// Used to decode the whole SSHSource as a single BinString + /// Used to decode the whole SSHSource as a single BinString ignoring the len field /// /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. fn dec_all_as_binstring(&mut self) -> WireResult> { Ok(BinString(self.take(self.buffer.len())?)) } + + /// Used to decode a slice of SSHSource as a single BinString ignoring the len field + /// + /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. + fn dec_as_binstring(&mut self, len: usize) -> WireResult> { + Ok(BinString(self.take(len)?)) + } } #[derive(Default)] @@ -242,39 +267,44 @@ impl<'a> SftpHandler<'a> { if let Some(mut write_tracker) = self.partial_write_request_tracker.take() { trace!( - "Processing successive chunks of a long write packet . Stored data: {:?}", + "Processing successive chunks of a long write packet. Stored data: {:?}", write_tracker ); + + let usable_data = in_len.min(write_tracker.remain_data_len as usize); + if in_len > write_tracker.remain_data_len as usize { // TODO: Investigate if we are receiving one packet and the beginning of the next one + let sftp_num = source.peak_packet_type_with_offset( + write_tracker.remain_data_len as usize, + )?; error!( - "There is too much data in the buffer! {:?} > than max expected {:?}", - in_len, write_tracker.remain_data_len + "There is too much data in the buffer! {:?} > than expected {:?}. There is a trailing packet in buffer: {:?}", + in_len, write_tracker.remain_data_len, sftp_num ); - return Err(WireError::PacketWrong); // TODO: Handle this error instead of failing. - } + }; - let current_write_offset = write_tracker.remain_data_offset; - let data_in_buffer = source.dec_all_as_binstring()?; + let data_segment = source.dec_as_binstring(usable_data)?; // TODO: Do proper casting with checks u32::try_from(data_in_buffer.0.len()) - let data_in_buffer_len = data_in_buffer.0.len() as u32; + let data_segment_len = data_segment.0.len() as u32; - write_tracker.remain_data_offset += data_in_buffer_len as u64; - write_tracker.remain_data_len -= data_in_buffer_len; + let current_write_offset = write_tracker.remain_data_offset; + write_tracker.remain_data_offset += data_segment_len as u64; + write_tracker.remain_data_len -= data_segment_len; let obscure_file_handle = write_tracker.get_file_handle(); debug!( - "Processing successive chunks of a long write packet. Writing : obscure_file_handle = {:?}, write_offset = {:?}, data = {:?}, data remaining = {:?}", + "Processing successive chunks of a long write packet. Writing : obscure_file_handle = {:?}, write_offset = {:?}, data_segment = {:?}, data remaining = {:?}", obscure_file_handle, current_write_offset, - data_in_buffer, + data_segment, write_tracker.remain_data_len ); match self.file_server.write( &obscure_file_handle, current_write_offset, - data_in_buffer.as_ref(), + data_segment.as_ref(), ) { Ok(_) => { if write_tracker.remain_data_len > 0 { From 1f41c8606b279406ee5cfcd78a6bb261c8b6c531 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 19 Sep 2025 15:21:21 +1000 Subject: [PATCH 287/393] WIP: Refactoring opaque_file_handle -> obscure_file_handle and adding traits Will not compile ATM First, I am still working on the details to make the use of traits to define the DemoSftpServer flexible. In this commit there is too much rigidity in the DemoSftpServer which does not allow my intended vision. Second, there is an unrelated refactoring here where SFTPSink and SftpSource have been extracted from sftphandle.rs to their own files for clarity --- demo/sftp/std/src/demofilehandlemanager.rs | 60 ++++++ demo/sftp/std/src/demoopaquefilehandle.rs | 21 ++ demo/sftp/std/src/demosftpserver.rs | 87 ++++---- demo/sftp/std/src/main.rs | 13 +- sftp/src/lib.rs | 6 +- sftp/src/obscured_file_handle.rs | 219 ------------------- sftp/src/opaquefilehandle.rs | 66 ++++++ sftp/src/proto.rs | 4 + sftp/src/sftphandle.rs | 238 +++------------------ sftp/src/sftpserver.rs | 29 ++- sftp/src/sftpsink.rs | 52 +++++ sftp/src/sftpsource.rs | 143 +++++++++++++ 12 files changed, 451 insertions(+), 487 deletions(-) create mode 100644 demo/sftp/std/src/demofilehandlemanager.rs create mode 100644 demo/sftp/std/src/demoopaquefilehandle.rs delete mode 100644 sftp/src/obscured_file_handle.rs create mode 100644 sftp/src/opaquefilehandle.rs create mode 100644 sftp/src/sftpsink.rs create mode 100644 sftp/src/sftpsource.rs diff --git a/demo/sftp/std/src/demofilehandlemanager.rs b/demo/sftp/std/src/demofilehandlemanager.rs new file mode 100644 index 00000000..e7bae95a --- /dev/null +++ b/demo/sftp/std/src/demofilehandlemanager.rs @@ -0,0 +1,60 @@ +use sunset_sftp::{ + OpaqueFileHandle, OpaqueFileHandleManager, PathFinder, StatusCode, +}; + +use std::collections::HashMap; // Not enforced. Only for std. For no_std environments other solutions can be used to store Key, Value + +pub struct DemoFileHandleManager +where + K: OpaqueFileHandle, + V: PathFinder, +{ + handle_map: HashMap, +} + +impl DemoFileHandleManager +where + K: OpaqueFileHandle, + V: PathFinder, +{ + pub fn new() -> Self { + Self { handle_map: HashMap::new() } + } +} + +impl OpaqueFileHandleManager for DemoFileHandleManager +where + K: OpaqueFileHandle, + V: PathFinder, +{ + type Error = StatusCode; + + fn insert(&mut self, private_handle: V, salt: &str) -> Result { + if self + .handle_map + .iter() + .any(|(_, private_handle)| private_handle.matches(&private_handle)) + { + return Err(StatusCode::SSH_FX_PERMISSION_DENIED); + } + + let handle = K::new( + format!("{:}-{:}", &private_handle.get_path_ref(), salt).as_str(), + ); + + self.handle_map.insert(handle.clone(), private_handle); + Ok(handle) + } + + fn remove(&mut self, opaque_handle: &K) -> Option { + self.handle_map.remove(opaque_handle) + } + + fn opaque_handle_exist(&self, opaque_handle: &K) -> bool { + self.handle_map.contains_key(opaque_handle) + } + + fn get_private_as_ref(&self, opaque_handle: &K) -> Option<&V> { + self.handle_map.get(opaque_handle) + } +} diff --git a/demo/sftp/std/src/demoopaquefilehandle.rs b/demo/sftp/std/src/demoopaquefilehandle.rs new file mode 100644 index 00000000..44ecc2fe --- /dev/null +++ b/demo/sftp/std/src/demoopaquefilehandle.rs @@ -0,0 +1,21 @@ +use sunset_sftp::{FileHandle, OpaqueFileHandle}; + +#[derive(Debug, Hash, PartialEq, Eq, Clone)] +pub struct DemoOpaqueFileHandle {} + +impl OpaqueFileHandle for DemoOpaqueFileHandle { + fn new(seed: &str) -> Self { + todo!("Add some logic to create a hash form the &str from {:}", seed) + } + + fn try_from(file_handle: &FileHandle<'_>) -> sunset::sshwire::WireResult { + todo!( + "Add some logic to handle the the conversion try_from {:?}", + file_handle + ) + } + + fn into_file_handle(&self) -> FileHandle<'_> { + todo!("Add some logic to handle the the conversion into_file_handle") + } +} diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index da291a84..52c96aaf 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -1,49 +1,60 @@ +use crate::demofilehandlemanager::DemoFileHandleManager; + use sunset_sftp::{ - Attrs, DirReply, Filename, HandleManager, Name, NameEntry, ObscuredFileHandle, - PathFinder, ReadReply, SftpOpResult, SftpServer, StatusCode, + Attrs, DirReply, Filename, Name, NameEntry, OpaqueFileHandle, + OpaqueFileHandleManager, PathFinder, ReadReply, SftpOpResult, SftpServer, + StatusCode, }; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; use std::{fs::File, os::unix::fs::FileExt}; -struct PrivateFileHandler { +pub(crate) struct PrivateFileHandler { file_path: String, permissions: Option, file: File, } +static OPAQUE_SALT: &'static str = "12d%32"; + impl PathFinder for PrivateFileHandler { - fn matches_path(&self, path: &str) -> bool { - self.file_path.as_str().eq_ignore_ascii_case(path) + fn matches(&self, path: &PrivateFileHandler) -> bool { + self.file_path.as_str().eq_ignore_ascii_case(path.get_path_ref()) + } + + fn get_path_ref(&self) -> &str { + self.file_path.as_str() } } -pub struct DemoSftpServer { +pub struct DemoSftpServer +where + K: OpaqueFileHandle, + V: PathFinder, +{ base_path: String, - handlers_manager: HandleManager, + handlers_manager: DemoFileHandleManager, } -impl DemoSftpServer { +impl DemoSftpServer +where + K: OpaqueFileHandle, + V: PathFinder, +{ pub fn new(base_path: String) -> Self { - DemoSftpServer { base_path, handlers_manager: HandleManager::new() } + DemoSftpServer { base_path, handlers_manager: DemoFileHandleManager::new() } } } -impl SftpServer<'_> for DemoSftpServer { - // Mocking an Open operation. Will not check for permissions - fn open( - &mut self, - filename: &str, - attrs: &Attrs, - ) -> SftpOpResult { +impl SftpServer<'_, K> for DemoSftpServer +where + K: OpaqueFileHandle, + V: PathFinder, +{ + fn open(&mut self, filename: &str, attrs: &Attrs) -> SftpOpResult { debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); - if self.handlers_manager.is_open(filename) { - warn!("File {:?} already open, won't allow it", filename); - return Err(StatusCode::SSH_FX_PERMISSION_DENIED); - } - let poxit_attr = attrs .permissions .as_ref() @@ -62,18 +73,21 @@ impl SftpServer<'_> for DemoSftpServer { .open(filename) .map_err(|_| StatusCode::SSH_FX_FAILURE)?; - let fh = self.handlers_manager.create_handle(PrivateFileHandler { - file_path: filename.into(), - permissions: attrs.permissions, - file, - }); + let fh = self.handlers_manager.insert( + PrivateFileHandler { + file_path: filename.into(), + permissions: attrs.permissions, + file, + }, + OPAQUE_SALT, + ); debug!( "Filename \"{:?}\" will have the obscured file handle: {:?}", filename, fh ); - Ok(fh) + fh } fn realpath(&mut self, dir: &str) -> SftpOpResult> { @@ -93,13 +107,8 @@ impl SftpServer<'_> for DemoSftpServer { }])) } - fn close( - &mut self, - obscure_file_handle: &ObscuredFileHandle, - ) -> SftpOpResult<()> { - if let Some(handle) = - self.handlers_manager.remove_handle(obscure_file_handle) - { + fn close(&mut self, obscure_file_handle: &K) -> SftpOpResult<()> { + if let Some(handle) = self.handlers_manager.remove(obscure_file_handle) { debug!( "SftpServer Close operation on {:?} was successful", handle.file_path @@ -117,13 +126,13 @@ impl SftpServer<'_> for DemoSftpServer { fn write( &mut self, - obscured_file_handle: &ObscuredFileHandle, + obscured_file_handle: &K, offset: u64, buf: &[u8], ) -> SftpOpResult<()> { let private_file_handle = self .handlers_manager - .get_handle_value_as_ref(obscured_file_handle) + .get_private_as_ref(obscured_file_handle) .ok_or(StatusCode::SSH_FX_FAILURE)?; let permissions_poxit = (private_file_handle @@ -160,7 +169,7 @@ impl SftpServer<'_> for DemoSftpServer { fn read( &mut self, - obscured_file_handle: &ObscuredFileHandle, + obscured_file_handle: &K, offset: u64, _reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { @@ -172,14 +181,14 @@ impl SftpServer<'_> for DemoSftpServer { Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - fn opendir(&mut self, dir: &str) -> SftpOpResult { + fn opendir(&mut self, dir: &str) -> SftpOpResult { log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } fn readdir( &mut self, - obscured_file_handle: &ObscuredFileHandle, + obscured_file_handle: &K, _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 54aadd00..4a9799c7 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -6,7 +6,10 @@ pub(crate) use sunset_demo_common as demo_common; use demo_common::{DemoCommon, DemoServer, SSHConfig}; -use crate::demosftpserver::DemoSftpServer; +use crate::{ + demoopaquefilehandle::DemoOpaqueFileHandle, + demosftpserver::{DemoSftpServer, PrivateFileHandler}, +}; use embedded_io_async::{Read, Write}; @@ -23,6 +26,8 @@ use embassy_sync::channel::Channel; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; +mod demofilehandlemanager; +mod demoopaquefilehandle; mod demosftpserver; const NUM_LISTENERS: usize = 4; @@ -152,7 +157,11 @@ impl DemoServer for StdDemo { "./demo/sftp/std/testing/out/".to_string(), ); - let mut sftp_handler = SftpHandler::new(&mut file_server); + let mut sftp_handler = + SftpHandler::< + DemoOpaqueFileHandle, + DemoSftpServer, + >::new(&mut file_server); loop { let lr = stdio.read(&mut buffer_in).await?; trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 16a8ba76..0b9278a1 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,7 +1,9 @@ -mod obscured_file_handle; +mod opaquefilehandle; mod proto; mod sftphandle; mod sftpserver; +mod sftpsink; +mod sftpsource; pub use sftpserver::DirReply; pub use sftpserver::ReadReply; @@ -10,7 +12,7 @@ pub use sftpserver::SftpServer; pub use sftphandle::SftpHandler; -pub use obscured_file_handle::{HandleManager, ObscuredFileHandle, PathFinder}; +pub use opaquefilehandle::{OpaqueFileHandle, OpaqueFileHandleManager, PathFinder}; pub use proto::Attrs; pub use proto::FileHandle; diff --git a/sftp/src/obscured_file_handle.rs b/sftp/src/obscured_file_handle.rs deleted file mode 100644 index 34a678d4..00000000 --- a/sftp/src/obscured_file_handle.rs +++ /dev/null @@ -1,219 +0,0 @@ -use crate::FileHandle; - -use sunset::sshwire::{BinString, WireError}; - -use std::collections::HashMap; - -pub const FILE_HANDLE_MAX_LEN: usize = 8; - -/// Obscured file handle using Linear Congruential Generator (LCG) for pseudo-random generation. -/// -/// This struct provides a fixed-length handle that appears random but is deterministic -/// based on the input seed. Uses LCG with constants from Numerical Recipes. -/// -/// # Limitations -/// -/// - **Not cryptographically secure**: Predictable if the algorithm and seed are known -/// - **Limited entropy**: u8 seed provides only 256 different possible handles -/// - **Linear correlations**: Sequential seeds produce statistically correlated outputs -/// - **Reversible**: Given the handle, the original seed can potentially be recovered -/// - **Not suitable for security**: Should only be used for obscuration, not protection -/// -/// # Security Note -/// -/// This is intended for basic handle obscuration in SFTP to prevent casual observation -/// of handle-to-file mappings. It is NOT suitable for cryptographic purposes. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ObscuredFileHandle { - data: [u8; FILE_HANDLE_MAX_LEN], -} - -impl ObscuredFileHandle { - /// Generate a pseudo-random handle from a u8 seed using Linear Congruential Generator. - /// - /// Same seed will always produce the same handle. Only 256 different handles - /// are possible due to u8 seed limitation. This is deliberate to imply its limitations - /// - /// TODO: If this library is to be hardened this is a point to address - pub fn new(seed: u8) -> Self { - let mut data = [0u8; FILE_HANDLE_MAX_LEN]; - - // Simple Linear Congruential Generator (LCG) - // Using constants from Numerical Recipes - let mut state = seed as u64; - - for chunk in data.chunks_mut(8) { - // LCG: next = (a * current + c) mod 2^64 - state = state.wrapping_mul(1664525).wrapping_add(1013904223); - - let bytes = state.to_le_bytes(); - let copy_len = chunk.len().min(8); - chunk[..copy_len].copy_from_slice(&bytes[..copy_len]); - } - - Self { data } - } - - /// Get the handle as a byte slice - pub fn as_bytes(&self) -> &[u8] { - &self.data - } - - /// Get the handle as a Vec for wire protocol - pub fn to_vec(&self) -> Vec { - self.data.to_vec() - } - - /// Create from existing bytes (for parsing from wire) - pub fn from_bytes(bytes: &[u8]) -> Option { - if bytes.len() != FILE_HANDLE_MAX_LEN { - return None; - } - - let mut data = [0u8; FILE_HANDLE_MAX_LEN]; - data.copy_from_slice(bytes); - Some(Self { data }) - } - - /// Create from BinString (for parsing from SFTP wire protocol) - pub fn from_binstring(binstring: &BinString<'_>) -> Option { - Self::from_bytes(binstring.0) - } - - /// Create from FileHandle (for parsing from SFTP wire protocol) - pub fn from_filehandle(file_handle: &FileHandle<'_>) -> Option { - Self::from_bytes(file_handle.0.0) - } - - /// Convert to BinString for SFTP wire protocol - pub fn to_binstring(&self) -> BinString<'_> { - BinString(&self.data) - } - - /// Convert to FileHandle for SFTP wire protocol - pub fn to_filehandle(&self) -> FileHandle<'_> { - FileHandle(self.to_binstring()) - } -} - -// Display trait for debugging/logging -impl core::fmt::Display for ObscuredFileHandle { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - for byte in &self.data { - write!(f, "{:02x}", byte)?; - } - Ok(()) - } -} - -// Standard trait implementations for BinString conversion -impl<'a> From<&'a ObscuredFileHandle> for BinString<'a> { - fn from(handle: &'a ObscuredFileHandle) -> Self { - handle.to_binstring() - } -} - -impl<'a> TryFrom<&BinString<'a>> for ObscuredFileHandle { - type Error = WireError; - - fn try_from(binstring: &BinString<'a>) -> Result { - Self::from_binstring(binstring).ok_or(WireError::BadString) - } -} - -impl<'a> TryFrom> for ObscuredFileHandle { - type Error = WireError; - - fn try_from(binstring: BinString<'a>) -> Result { - Self::try_from(&binstring) - } -} - -// Conversions with proto::FileHandle -impl<'a> From<&'a ObscuredFileHandle> for crate::proto::FileHandle<'a> { - fn from(handle: &'a ObscuredFileHandle) -> Self { - crate::proto::FileHandle(handle.into()) - } -} - -impl<'a> TryFrom<&crate::proto::FileHandle<'a>> for ObscuredFileHandle { - type Error = WireError; - - fn try_from( - file_handle: &crate::proto::FileHandle<'a>, - ) -> Result { - Self::try_from(&file_handle.0) - } -} - -impl<'a> TryFrom> for ObscuredFileHandle { - type Error = WireError; - - fn try_from( - file_handle: crate::proto::FileHandle<'a>, - ) -> Result { - Self::try_from(&file_handle) - } -} - -/// Used to standarise finding a path within the HandleManager -pub trait PathFinder { - /// Helper function to find elements stored in the HandleManager that matches the give path - fn matches_path(&self, path: &str) -> bool; -} - -// Example usage structure for managing handles -pub struct HandleManager -where - T: PathFinder, -{ - next_handle_id: u8, - handle_map: HashMap, -} - -impl HandleManager { - pub fn new() -> Self { - Self { next_handle_id: 1, handle_map: HashMap::new() } - } - - pub fn create_handle(&mut self, value: T) -> ObscuredFileHandle { - let handle = ObscuredFileHandle::new(self.next_handle_id); - self.next_handle_id += 1; - - self.handle_map.insert(handle, value); - handle - } - - pub fn get_handle_value_as_ref( - &self, - handle: &ObscuredFileHandle, - ) -> Option<&T> { - self.handle_map.get(handle) - } - - pub fn remove_handle(&mut self, handle: &ObscuredFileHandle) -> Option { - self.handle_map.remove(handle) - } - - pub fn handle_exists(&self, handle: &ObscuredFileHandle) -> bool { - self.handle_map.contains_key(handle) - } - - pub fn is_open(&self, filename: &str) -> bool { - if self.handle_map.is_empty() { - return false; - } - self.handle_map.iter().any(|(_, element)| element.matches_path(filename)) - // TODO: Fix this. We need to be able to find out if the filename has been open. That cannot be done with a general T. Is will need to implement a trait to check that - // true - } -} - -impl Default for HandleManager -where - T: PathFinder, -{ - fn default() -> Self { - Self::new() - } -} diff --git a/sftp/src/opaquefilehandle.rs b/sftp/src/opaquefilehandle.rs new file mode 100644 index 00000000..2bdf739b --- /dev/null +++ b/sftp/src/opaquefilehandle.rs @@ -0,0 +1,66 @@ +use crate::FileHandle; + +use sunset::sshwire::WireResult; + +/// This is the trait with the required methods for interoperability between different opaque file handles +pub trait OpaqueFileHandle: + Sized + Clone + core::hash::Hash + PartialEq + Eq + core::fmt::Debug +{ + /// Creates a new instance using a given string slice as `seed` which + /// content should not clearly related to the seed + fn new(seed: &str) -> Self; + + /// Creates a new `OpaqueFileHandleTrait` copying the content of the `FileHandle` + fn try_from(file_handle: &FileHandle<'_>) -> WireResult; + + /// Returns a FileHandle pointing to the data in the `OpaqueFileHandleTrait` Implementation + fn into_file_handle(&self) -> FileHandle<'_>; +} + +/// Used to standardize finding a path within the HandleManager +/// +/// Must be implemented by the private handle structure to allow the `OpaqueHandleManager` to look for the path of the file itself +pub trait PathFinder { + /// Helper function to find elements stored in the HandleManager that matches the give path + fn matches(&self, path: &Self) -> bool; + + /// gets the path as a reference + fn get_path_ref(&self) -> &str; +} + +/// This trait is used to manage the OpaqueFile +/// +/// The SFTP module user is not required to use it but instead is a suggestion for an exchangeable +/// trait that facilitates structuring the store and retrieve of 'OpaqueFileHandleTrait' (K), +/// together with a private handle type or structure (V) that will contains all the details internally stored for the given file. +/// +/// The only requisite for v is that implements PathFinder, which in fact is another suggested helper to allow the `OpaqueHandleManager` +/// to look for the file path. +pub trait OpaqueFileHandleManager +where + K: OpaqueFileHandle, + V: PathFinder, +{ + /// The error used for all the trait members returning an error + type Error; + + // Excluded since it is too restrictive + // /// Performs any HandleManager Initialization + // fn new() -> Self; + + /// Given the private_handle, stores it and return an opaque file handle + /// + /// Returns an error if the private_handle has a matching path as obtained from `PathFinder` + /// + /// Salt has been added to allow the user to add a factor that will mask how the opaque handle is generated + fn insert(&mut self, private_handle: V, salt: &str) -> Result; + + /// + fn remove(&mut self, opaque_handle: &K) -> Option; + + /// Returns true if the opaque handle exist + fn opaque_handle_exist(&self, opaque_handle: &K) -> bool; + + /// given the opaque_handle returns a reference to the associated private handle + fn get_private_as_ref(&self, opaque_handle: &K) -> Option<&V>; +} diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 252f1b4b..01abe820 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -10,15 +10,19 @@ use num_enum::FromPrimitive; use paste::paste; /// SFTP Minimum packet length is 9 bytes corresponding with `SSH_FXP_INIT` +#[allow(unused)] pub const SFTP_MINIMUM_PACKET_LEN: usize = 9; /// SFTP packets have the packet type after a u32 length field +#[allow(unused)] pub const SFTP_FIELD_ID_INDEX: usize = 4; /// SFTP packets ID length is 1 byte // pub const SFTP_FIELD_ID_LEN: usize = 1; /// SFTP packets start with the length field +#[allow(unused)] pub const SFTP_FIELD_LEN_INDEX: usize = 0; /// SFTP packets length field us u32 +#[allow(unused)] pub const SFTP_FIELD_LEN_LENGTH: usize = 4; // SSH_FXP_WRITE SFTP Packet definition used to decode long packets that do not fit in one buffer diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 08cc031a..0d2357c0 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,14 +1,13 @@ +use crate::OpaqueFileHandle; use crate::proto::{ - self, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_LENGTH, - SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SFTP_WRITE_REQID_INDEX, SftpNum, + self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, SftpPacket, Status, StatusCode, }; use crate::sftpserver::SftpServer; -use crate::{FileHandle, ObscuredFileHandle}; +use crate::sftpsink::SftpSink; +use crate::sftpsource::SftpSource; -use sunset::sshwire::{ - BinString, SSHDecode, SSHSink, SSHSource, WireError, WireResult, -}; +use sunset::sshwire::{SSHDecode, WireError, WireResult}; use core::u32; #[allow(unused_imports)] @@ -17,17 +16,17 @@ use std::usize; /// Used to keep record of a long SFTP Write request that does not fit in receiving buffer and requires processing in batches #[derive(Debug)] -struct PartialWriteRequestTracker { +pub struct PartialWriteRequestTracker { req_id: ReqId, - obscure_file_handle: ObscuredFileHandle, // TODO: Change the file handle in SftpServer functions signature so it has a sort fixed length handle. + obscure_file_handle: T, remain_data_len: u32, remain_data_offset: u64, } -impl PartialWriteRequestTracker { +impl PartialWriteRequestTracker { pub fn new( req_id: ReqId, - obscure_file_handle: ObscuredFileHandle, + obscure_file_handle: T, remain_data_len: u32, remain_data_offset: u64, ) -> WireResult { @@ -39,212 +38,35 @@ impl PartialWriteRequestTracker { }) } - pub fn get_file_handle(&self) -> ObscuredFileHandle { + pub fn get_file_handle(&self) -> T { self.obscure_file_handle.clone() } } -/// SftpSource implements SSHSource and also extra functions to handle some challenges with long SFTP packets in constrained environments -#[derive(Default, Debug)] -pub struct SftpSource<'de> { - pub buffer: &'de [u8], - pub index: usize, -} - -impl<'de> SSHSource<'de> for SftpSource<'de> { - // Original take - fn take(&mut self, len: usize) -> sunset::sshwire::WireResult<&'de [u8]> { - if len + self.index > self.buffer.len() { - return Err(WireError::RanOut); - } - let original_index = self.index; - let slice = &self.buffer[self.index..self.index + len]; - self.index += len; - trace!( - "slice returned: {:?}. original index {:?}, new index: {:?}", - slice, original_index, self.index - ); - Ok(slice) - } - - fn remaining(&self) -> usize { - self.buffer.len() - self.index - } - - fn ctx(&mut self) -> &mut sunset::packets::ParseContext { - todo!("Which context for sftp?"); - } -} - -impl<'de> SftpSource<'de> { - pub fn new(buffer: &'de [u8]) -> Self { - SftpSource { buffer: buffer, index: 0 } - } - - /// Peaks the buffer for packet type. This does not advance the reading index - /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail - /// - /// **Warning**: will only work in well formed packets, in other case the result will contain garbage - fn peak_packet_type(&self) -> WireResult { - // const SFTP_ID_BUFFER_INDEX: usize = 4; // All SFTP packet have the packet type after a u32 length field - // const SFTP_MINIMUM_LENGTH: usize = 9; // Corresponds to a minimal SSH_FXP_INIT packet - if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - Err(WireError::PacketWrong) - } else { - Ok(SftpNum::from(self.buffer[SFTP_FIELD_ID_INDEX])) - } - } - - /// Peaks the buffer for packet type adding an offset. This does not advance the reading index - /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail - /// - /// **Warning**: This might only work in special conditions, such as those where the , in other case the result will contain garbage - fn peak_packet_type_with_offset( - &self, - starting_offset: usize, - ) -> WireResult { - // const SFTP_ID_BUFFER_INDEX: usize = 4; // All SFTP packet have the packet type after a u32 length field - // const SFTP_MINIMUM_LENGTH: usize = 9; // Corresponds to a minimal SSH_FXP_INIT packet - if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - Err(WireError::PacketWrong) - } else { - Ok(SftpNum::from(self.buffer[starting_offset + SFTP_FIELD_ID_INDEX])) - } - } - - /// Assuming that the buffer contains a Write request packet initial bytes, Peaks the buffer for the handle length. This does not advance the reading index - /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail - /// - /// **Warning**: will only work in well formed write packets, in other case the result will contain garbage - fn get_packet_partial_write_content_and_tracker( - &mut self, - ) -> WireResult<( - ObscuredFileHandle, - ReqId, - u64, - BinString<'de>, - PartialWriteRequestTracker, - )> { - if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - Err(WireError::PacketWrong) - } else { - let prev_index = self.index; - self.index = SFTP_WRITE_REQID_INDEX; - let req_id = ReqId::dec(self)?; - let file_handle = FileHandle::dec(self)?; - - let obscured_file_handle = - ObscuredFileHandle::from_binstring(&file_handle.0) - .ok_or(WireError::BadString)?; - - let offset = u64::dec(self)?; - let data_len = u32::dec(self)?; - - let data_len_in_buffer = self.buffer.len() - self.index; - let data_in_buffer = BinString(self.take(data_len_in_buffer)?); - - self.index = prev_index; - - let remain_data_len = data_len - data_len_in_buffer as u32; - let remain_data_offset = offset + data_len_in_buffer as u64; - trace!( - "Request ID = {:?}, Handle = {:?}, offset = {:?}, data length in buffer = {:?}, data in current buffer {:?} ", - req_id, file_handle, offset, data_len_in_buffer, data_in_buffer - ); - - let write_tracker = PartialWriteRequestTracker::new( - req_id, - ObscuredFileHandle::from_filehandle(&file_handle) - .ok_or(WireError::BadString)?, - remain_data_len, - remain_data_offset, - )?; - - Ok((obscured_file_handle, req_id, offset, data_in_buffer, write_tracker)) - } - } - - /// Used to decode the whole SSHSource as a single BinString ignoring the len field - /// - /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. - fn dec_all_as_binstring(&mut self) -> WireResult> { - Ok(BinString(self.take(self.buffer.len())?)) - } - - /// Used to decode a slice of SSHSource as a single BinString ignoring the len field - /// - /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. - fn dec_as_binstring(&mut self, len: usize) -> WireResult> { - Ok(BinString(self.take(len)?)) - } -} - -#[derive(Default)] -pub struct SftpSink<'g> { - pub buffer: &'g mut [u8], - index: usize, -} - -impl<'g> SftpSink<'g> { - pub fn new(s: &'g mut [u8]) -> Self { - SftpSink { buffer: s, index: SFTP_FIELD_LEN_LENGTH } - } - - /// Finalise the buffer by prepending the payload size and returning - /// - /// Returns the final index in the buffer as a reference for the space used - pub fn finalize(&mut self) -> usize { - if self.index <= SFTP_FIELD_LEN_LENGTH { - warn!("SftpSink trying to terminate it before pushing data"); - return 0; - } // size is 0 - let used_size = (self.index - SFTP_FIELD_LEN_LENGTH) as u32; - - used_size - .to_be_bytes() - .iter() - .enumerate() - .for_each(|(i, v)| self.buffer[i] = *v); - - self.index - } -} - -impl<'g> SSHSink for SftpSink<'g> { - fn push(&mut self, v: &[u8]) -> sunset::sshwire::WireResult<()> { - if v.len() + self.index > self.buffer.len() { - return Err(WireError::NoRoom); - } - trace!("Sink index: {:}", self.index); - v.iter().for_each(|val| { - self.buffer[self.index] = *val; - self.index += 1; - }); - trace!("Sink new index: {:}", self.index); - Ok(()) - } -} - //#[derive(Debug, Clone)] -pub struct SftpHandler<'a> { +pub struct SftpHandler<'a, T, S> +where + T: OpaqueFileHandle, + S: SftpServer<'a, T>, +{ /// The local SFTP File server implementing the basic SFTP requests defined by `SftpServer` - file_server: &'a mut dyn SftpServer<'a>, + file_server: &'a mut S, /// Once the client and the server have verified the agreed SFTP version the session is initialized initialized: bool, /// Use to process SFTP Write packets that have been received partially and the remaining is expected in successive buffers - partial_write_request_tracker: Option, + partial_write_request_tracker: Option>, } -impl<'a> SftpHandler<'a> { - pub fn new(file_server: &'a mut impl SftpServer<'a>) -> Self { +impl<'a, T, S> SftpHandler<'a, T, S> +where + T: OpaqueFileHandle, + S: SftpServer<'a, T>, +{ + pub fn new(file_server: &'a mut S) -> Self { SftpHandler { file_server, - initialized: false, partial_write_request_tracker: None, } @@ -274,7 +96,7 @@ impl<'a> SftpHandler<'a> { let usable_data = in_len.min(write_tracker.remain_data_len as usize); if in_len > write_tracker.remain_data_len as usize { - // TODO: Investigate if we are receiving one packet and the beginning of the next one + // This is because we are receiving one packet and the beginning of the next one let sftp_num = source.peak_packet_type_with_offset( write_tracker.remain_data_len as usize, )?; @@ -454,11 +276,11 @@ impl<'a> SftpHandler<'a> { } SftpPacket::Open(req_id, open) => { match self.file_server.open(open.filename.as_str()?, &open.attrs) { - Ok(obscured_file_handle) => { + Ok(opaque_file_handle) => { let response = SftpPacket::Handle( req_id, proto::Handle { - handle: obscured_file_handle.to_filehandle(), + handle: opaque_file_handle.into_file_handle(), }, ); response.encode_response(sink)?; @@ -472,8 +294,7 @@ impl<'a> SftpHandler<'a> { } SftpPacket::Write(req_id, write) => { match self.file_server.write( - &ObscuredFileHandle::from_filehandle(&write.handle) - .ok_or(WireError::BadString)?, + &T::try_from(&write.handle)?, write.offset, write.data.as_ref(), ) { @@ -485,10 +306,7 @@ impl<'a> SftpHandler<'a> { }; } SftpPacket::Close(req_id, close) => { - match self.file_server.close( - &ObscuredFileHandle::from_filehandle(&close.handle) - .ok_or(WireError::BadString)?, - ) { + match self.file_server.close(&T::try_from(&close.handle)?) { Ok(_) => push_ok(req_id, sink)?, Err(e) => { error!("SFTP Close thrown: {:?}", e); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 37a51949..201c7eb3 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,5 +1,5 @@ use crate::{ - ObscuredFileHandle, + OpaqueFileHandle, proto::{Attrs, Name, StatusCode}, }; @@ -11,13 +11,12 @@ pub type SftpOpResult = core::result::Result; /// Some less core operations have a Provided implementation returning /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. -pub trait SftpServer<'a> { +pub trait SftpServer<'a, T> +where + T: OpaqueFileHandle, +{ /// Opens a file or directory for reading/writing - fn open( - &'_ mut self, - filename: &str, - attrs: &Attrs, - ) -> SftpOpResult { + fn open(&'_ mut self, filename: &str, attrs: &Attrs) -> SftpOpResult { log::error!( "SftpServer Open operation not defined: filename = {:?}, attrs = {:?}", filename, @@ -27,7 +26,7 @@ pub trait SftpServer<'a> { } /// Close either a file or directory handle - fn close(&mut self, handle: &ObscuredFileHandle) -> SftpOpResult<()> { + fn close(&mut self, handle: &T) -> SftpOpResult<()> { log::error!("SftpServer Close operation not defined: handle = {:?}", handle); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -35,13 +34,13 @@ pub trait SftpServer<'a> { fn read( &mut self, - obscured_file_handle: &ObscuredFileHandle, + opaque_file_handle: &T, offset: u64, _reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", - obscured_file_handle, + opaque_file_handle, offset ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -49,32 +48,32 @@ pub trait SftpServer<'a> { fn write( &mut self, - obscured_file_handle: &ObscuredFileHandle, + opaque_file_handle: &T, offset: u64, buf: &[u8], ) -> SftpOpResult<()> { log::error!( "SftpServer Write operation: handle = {:?}, offset = {:?}, buf = {:?}", - obscured_file_handle, + opaque_file_handle, offset, String::from_utf8(buf.to_vec()) ); Ok(()) } - fn opendir(&mut self, dir: &str) -> SftpOpResult { + fn opendir(&mut self, dir: &str) -> SftpOpResult { log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } fn readdir( &mut self, - obscured_file_handle: &ObscuredFileHandle, + opaque_file_handle: &T, _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", - obscured_file_handle + opaque_file_handle ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } diff --git a/sftp/src/sftpsink.rs b/sftp/src/sftpsink.rs new file mode 100644 index 00000000..f3bb85c3 --- /dev/null +++ b/sftp/src/sftpsink.rs @@ -0,0 +1,52 @@ +use crate::proto::SFTP_FIELD_LEN_LENGTH; + +use sunset::sshwire::{SSHSink, WireError}; + +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; + +#[derive(Default)] +pub struct SftpSink<'g> { + pub buffer: &'g mut [u8], + index: usize, +} + +impl<'g> SftpSink<'g> { + pub fn new(s: &'g mut [u8]) -> Self { + SftpSink { buffer: s, index: SFTP_FIELD_LEN_LENGTH } + } + + /// Finalise the buffer by prepending the payload size and returning + /// + /// Returns the final index in the buffer as a reference for the space used + pub fn finalize(&mut self) -> usize { + if self.index <= SFTP_FIELD_LEN_LENGTH { + warn!("SftpSink trying to terminate it before pushing data"); + return 0; + } // size is 0 + let used_size = (self.index - SFTP_FIELD_LEN_LENGTH) as u32; + + used_size + .to_be_bytes() + .iter() + .enumerate() + .for_each(|(i, v)| self.buffer[i] = *v); + + self.index + } +} + +impl<'g> SSHSink for SftpSink<'g> { + fn push(&mut self, v: &[u8]) -> sunset::sshwire::WireResult<()> { + if v.len() + self.index > self.buffer.len() { + return Err(WireError::NoRoom); + } + trace!("Sink index: {:}", self.index); + v.iter().for_each(|val| { + self.buffer[self.index] = *val; + self.index += 1; + }); + trace!("Sink new index: {:}", self.index); + Ok(()) + } +} diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs new file mode 100644 index 00000000..9c97cc85 --- /dev/null +++ b/sftp/src/sftpsource.rs @@ -0,0 +1,143 @@ +use crate::proto::{ + ReqId, SFTP_FIELD_ID_INDEX, SFTP_MINIMUM_PACKET_LEN, SFTP_WRITE_REQID_INDEX, + SftpNum, +}; +use crate::sftphandle::PartialWriteRequestTracker; +use crate::{FileHandle, OpaqueFileHandle}; + +use sunset::sshwire::{BinString, SSHDecode, SSHSource, WireError, WireResult}; + +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; + +/// SftpSource implements SSHSource and also extra functions to handle some challenges with long SFTP packets in constrained environments +#[derive(Default, Debug)] +pub struct SftpSource<'de> { + pub buffer: &'de [u8], + pub index: usize, +} + +impl<'de> SSHSource<'de> for SftpSource<'de> { + // Original take + fn take(&mut self, len: usize) -> sunset::sshwire::WireResult<&'de [u8]> { + if len + self.index > self.buffer.len() { + return Err(WireError::RanOut); + } + let original_index = self.index; + let slice = &self.buffer[self.index..self.index + len]; + self.index += len; + trace!( + "slice returned: {:?}. original index {:?}, new index: {:?}", + slice, original_index, self.index + ); + Ok(slice) + } + + fn remaining(&self) -> usize { + self.buffer.len() - self.index + } + + fn ctx(&mut self) -> &mut sunset::packets::ParseContext { + todo!("Which context for sftp?"); + } +} + +impl<'de> SftpSource<'de> { + pub fn new(buffer: &'de [u8]) -> Self { + SftpSource { buffer: buffer, index: 0 } + } + + /// Peaks the buffer for packet type. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: will only work in well formed packets, in other case the result will contain garbage + pub(crate) fn peak_packet_type(&self) -> WireResult { + if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + Err(WireError::PacketWrong) + } else { + Ok(SftpNum::from(self.buffer[SFTP_FIELD_ID_INDEX])) + } + } + + /// Peaks the buffer for packet type adding an offset. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: This might only work in special conditions, such as those where the , in other case the result will contain garbage + pub(crate) fn peak_packet_type_with_offset( + &self, + starting_offset: usize, + ) -> WireResult { + // const SFTP_ID_BUFFER_INDEX: usize = 4; // All SFTP packet have the packet type after a u32 length field + // const SFTP_MINIMUM_LENGTH: usize = 9; // Corresponds to a minimal SSH_FXP_INIT packet + if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + Err(WireError::PacketWrong) + } else { + Ok(SftpNum::from(self.buffer[starting_offset + SFTP_FIELD_ID_INDEX])) + } + } + + /// Assuming that the buffer contains a Write request packet initial bytes, Peaks the buffer for the handle length. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: will only work in well formed write packets, in other case the result will contain garbage + pub(crate) fn get_packet_partial_write_content_and_tracker< + T: OpaqueFileHandle, + >( + &mut self, + ) -> WireResult<(T, ReqId, u64, BinString<'de>, PartialWriteRequestTracker)> + { + if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + Err(WireError::PacketWrong) + } else { + let prev_index = self.index; + self.index = SFTP_WRITE_REQID_INDEX; + let req_id = ReqId::dec(self)?; + let file_handle = FileHandle::dec(self)?; + + let obscured_file_handle = OpaqueFileHandle::try_from(&file_handle)?; + let offset = u64::dec(self)?; + let data_len = u32::dec(self)?; + + let data_len_in_buffer = self.buffer.len() - self.index; + let data_in_buffer = BinString(self.take(data_len_in_buffer)?); + + self.index = prev_index; + + let remain_data_len = data_len - data_len_in_buffer as u32; + let remain_data_offset = offset + data_len_in_buffer as u64; + trace!( + "Request ID = {:?}, Handle = {:?}, offset = {:?}, data length in buffer = {:?}, data in current buffer {:?} ", + req_id, file_handle, offset, data_len_in_buffer, data_in_buffer + ); + + let write_tracker = PartialWriteRequestTracker::new( + req_id, + OpaqueFileHandle::try_from(&file_handle)?, + remain_data_len, + remain_data_offset, + )?; + + Ok((obscured_file_handle, req_id, offset, data_in_buffer, write_tracker)) + } + } + + /// Used to decode the whole SSHSource as a single BinString ignoring the len field + /// + /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. + pub(crate) fn dec_all_as_binstring(&mut self) -> WireResult> { + Ok(BinString(self.take(self.buffer.len())?)) + } + + /// Used to decode a slice of SSHSource as a single BinString ignoring the len field + /// + /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. + pub(crate) fn dec_as_binstring( + &mut self, + len: usize, + ) -> WireResult> { + Ok(BinString(self.take(len)?)) + } +} From 16384ea42461c4bfff041cd872ddce0df80be38b Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 19 Sep 2025 16:36:47 +1000 Subject: [PATCH 288/393] WIP: Fixed DemoSftpServer definition to use a custom implementation This allows the library user to define both a private and opaque file handle --- demo/sftp/std/src/demosftpserver.rs | 50 ++++++++++++++--------------- demo/sftp/std/src/main.rs | 7 ++-- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 52c96aaf..aea93817 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -1,9 +1,11 @@ -use crate::demofilehandlemanager::DemoFileHandleManager; +use crate::{ + demofilehandlemanager::DemoFileHandleManager, + demoopaquefilehandle::DemoOpaqueFileHandle, +}; use sunset_sftp::{ - Attrs, DirReply, Filename, Name, NameEntry, OpaqueFileHandle, - OpaqueFileHandleManager, PathFinder, ReadReply, SftpOpResult, SftpServer, - StatusCode, + Attrs, DirReply, Filename, Name, NameEntry, OpaqueFileHandleManager, PathFinder, + ReadReply, SftpOpResult, SftpServer, StatusCode, }; #[allow(unused_imports)] @@ -28,31 +30,24 @@ impl PathFinder for PrivateFileHandler { } } -pub struct DemoSftpServer -where - K: OpaqueFileHandle, - V: PathFinder, -{ +pub struct DemoSftpServer { base_path: String, - handlers_manager: DemoFileHandleManager, + handlers_manager: + DemoFileHandleManager, } -impl DemoSftpServer -where - K: OpaqueFileHandle, - V: PathFinder, -{ +impl DemoSftpServer { pub fn new(base_path: String) -> Self { DemoSftpServer { base_path, handlers_manager: DemoFileHandleManager::new() } } } -impl SftpServer<'_, K> for DemoSftpServer -where - K: OpaqueFileHandle, - V: PathFinder, -{ - fn open(&mut self, filename: &str, attrs: &Attrs) -> SftpOpResult { +impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { + fn open( + &mut self, + filename: &str, + attrs: &Attrs, + ) -> SftpOpResult { debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); let poxit_attr = attrs @@ -107,7 +102,10 @@ where }])) } - fn close(&mut self, obscure_file_handle: &K) -> SftpOpResult<()> { + fn close( + &mut self, + obscure_file_handle: &DemoOpaqueFileHandle, + ) -> SftpOpResult<()> { if let Some(handle) = self.handlers_manager.remove(obscure_file_handle) { debug!( "SftpServer Close operation on {:?} was successful", @@ -126,7 +124,7 @@ where fn write( &mut self, - obscured_file_handle: &K, + obscured_file_handle: &DemoOpaqueFileHandle, offset: u64, buf: &[u8], ) -> SftpOpResult<()> { @@ -169,7 +167,7 @@ where fn read( &mut self, - obscured_file_handle: &K, + obscured_file_handle: &DemoOpaqueFileHandle, offset: u64, _reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { @@ -181,14 +179,14 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - fn opendir(&mut self, dir: &str) -> SftpOpResult { + fn opendir(&mut self, dir: &str) -> SftpOpResult { log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } fn readdir( &mut self, - obscured_file_handle: &K, + obscured_file_handle: &DemoOpaqueFileHandle, _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 4a9799c7..0644aa67 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -158,10 +158,9 @@ impl DemoServer for StdDemo { ); let mut sftp_handler = - SftpHandler::< - DemoOpaqueFileHandle, - DemoSftpServer, - >::new(&mut file_server); + SftpHandler::::new( + &mut file_server, + ); loop { let lr = stdio.read(&mut buffer_in).await?; trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); From fe9152b5174440b38a6d60427e71333670021e94 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 19 Sep 2025 17:10:20 +1000 Subject: [PATCH 289/393] WIP: Minimal implementation for DemoSftpServer Still a work in progress, but extract the definition of: - OpaqueFileHandle trait struct: to allow library users to define the OpaqueFileHandle at will - OpaqueFileHandle trait struct: A proposed structure trait to allow the users defining its own OpaqueFileHandle and InternalHandle Manager This increase the flexibility of the library allowing users taking decisions of their application rather than forcing them to use predefined elements Next step: Handle consecutive requests that might be received in the same buffer --- demo/sftp/std/Cargo.toml | 1 + demo/sftp/std/src/demoopaquefilehandle.rs | 29 +++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/demo/sftp/std/Cargo.toml b/demo/sftp/std/Cargo.toml index 0c2a8205..1cce9708 100644 --- a/demo/sftp/std/Cargo.toml +++ b/demo/sftp/std/Cargo.toml @@ -37,3 +37,4 @@ async-io = "1.6.0" critical-section = "1.1" rand = { version = "0.8", default-features = false, features = ["getrandom"] } sha2 = { version = "0.10", default-features = false } +fnv = "1.0.7" diff --git a/demo/sftp/std/src/demoopaquefilehandle.rs b/demo/sftp/std/src/demoopaquefilehandle.rs index 44ecc2fe..b7a1b92c 100644 --- a/demo/sftp/std/src/demoopaquefilehandle.rs +++ b/demo/sftp/std/src/demoopaquefilehandle.rs @@ -1,21 +1,36 @@ use sunset_sftp::{FileHandle, OpaqueFileHandle}; +use sunset::sshwire::{BinString, WireError}; + +use core::hash::Hasher; + +use fnv::FnvHasher; + +const HASH_LEN: usize = 4; #[derive(Debug, Hash, PartialEq, Eq, Clone)] -pub struct DemoOpaqueFileHandle {} +pub(crate) struct DemoOpaqueFileHandle { + tiny_hash: [u8; HASH_LEN], +} impl OpaqueFileHandle for DemoOpaqueFileHandle { fn new(seed: &str) -> Self { - todo!("Add some logic to create a hash form the &str from {:}", seed) + let mut hasher = FnvHasher::default(); + hasher.write(seed.as_bytes()); + DemoOpaqueFileHandle { tiny_hash: (hasher.finish() as u32).to_be_bytes() } } fn try_from(file_handle: &FileHandle<'_>) -> sunset::sshwire::WireResult { - todo!( - "Add some logic to handle the the conversion try_from {:?}", - file_handle - ) + if !file_handle.0 .0.len().eq(&core::mem::size_of::()) + { + return Err(WireError::BadString); + } + + let mut tiny_hash = [0u8; HASH_LEN]; + tiny_hash.copy_from_slice(file_handle.0 .0); + Ok(DemoOpaqueFileHandle { tiny_hash }) } fn into_file_handle(&self) -> FileHandle<'_> { - todo!("Add some logic to handle the the conversion into_file_handle") + FileHandle(BinString(&self.tiny_hash)) } } From 43f58207da524c7f1f41b96178f4158346b6f71e Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 19 Sep 2025 17:23:51 +1000 Subject: [PATCH 290/393] opaque_file_handle not obscure_file_handle --- demo/sftp/std/src/demosftpserver.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index aea93817..79591175 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -104,9 +104,9 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { fn close( &mut self, - obscure_file_handle: &DemoOpaqueFileHandle, + opaque_file_handle: &DemoOpaqueFileHandle, ) -> SftpOpResult<()> { - if let Some(handle) = self.handlers_manager.remove(obscure_file_handle) { + if let Some(handle) = self.handlers_manager.remove(opaque_file_handle) { debug!( "SftpServer Close operation on {:?} was successful", handle.file_path @@ -116,7 +116,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } else { error!( "SftpServer Close operation on handle {:?} failed", - obscure_file_handle + opaque_file_handle ); Err(StatusCode::SSH_FX_FAILURE) } @@ -124,13 +124,13 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { fn write( &mut self, - obscured_file_handle: &DemoOpaqueFileHandle, + opaque_file_handle: &DemoOpaqueFileHandle, offset: u64, buf: &[u8], ) -> SftpOpResult<()> { let private_file_handle = self .handlers_manager - .get_private_as_ref(obscured_file_handle) + .get_private_as_ref(opaque_file_handle) .ok_or(StatusCode::SSH_FX_FAILURE)?; let permissions_poxit = (private_file_handle @@ -143,7 +143,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { log::trace!( "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buf = {:?}", - obscured_file_handle, + opaque_file_handle, private_file_handle.file_path, offset, String::from_utf8(buf.to_vec()) @@ -155,7 +155,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { log::debug!( "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buffer length = {:?}, bytes written = {:?}", - obscured_file_handle, + opaque_file_handle, private_file_handle.file_path, offset, buf.len(), @@ -167,13 +167,13 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { fn read( &mut self, - obscured_file_handle: &DemoOpaqueFileHandle, + opaque_file_handle: &DemoOpaqueFileHandle, offset: u64, _reply: &mut ReadReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", - obscured_file_handle, + opaque_file_handle, offset ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -186,12 +186,12 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { fn readdir( &mut self, - obscured_file_handle: &DemoOpaqueFileHandle, + opaque_file_handle: &DemoOpaqueFileHandle, _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", - obscured_file_handle + opaque_file_handle ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } From 76c2789c111a9ac463e26cd1dcf00dce7237cd20 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 19 Sep 2025 18:07:38 +1000 Subject: [PATCH 291/393] Letting the `sftp_loop` and `prog_loop` finish DemoServer.run() This way the common server closes the socket and SFTP clients do not get hanged on failure or bye --- demo/sftp/std/src/main.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 0644aa67..63be11e3 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -181,9 +181,11 @@ impl DemoServer for StdDemo { } { Ok(_) => { warn!("sftp server loop finished gracefully"); + return Ok(()); } Err(e) => { - warn!("sftp server loop finished with an error: {}", e) + error!("sftp server loop finished with an error: {}", e); + return Err(e); } }; } @@ -191,8 +193,16 @@ impl DemoServer for StdDemo { }; let selected = select(prog_loop, sftp_loop).await; - error!("Selected finished: {:?}", selected); - todo!("Loop terminated: {:?}", selected) + match selected { + embassy_futures::select::Either::First(res) => { + warn!("prog_loop finished: {:?}", res); + res + } + embassy_futures::select::Either::Second(res) => { + warn!("sftp_loop finished: {:?}", res); + res + } + } } } From dfa66dd011b6cc51dc731bdcea12a401030d8869 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 23 Sep 2025 20:05:09 +1000 Subject: [PATCH 292/393] Tiding up Added change to Cargo.lock is part of commit a19b0c6aa5ffa2ddb19296d97eed0d15519b3972 Removed cargo-example-sftp.rs, used for macro_rules troubleshooting not required at the moment --- Cargo.lock | 1 + sftp/out/cargo-expand-sftp.rs | 1800 --------------------------------- 2 files changed, 1 insertion(+), 1800 deletions(-) delete mode 100644 sftp/out/cargo-expand-sftp.rs diff --git a/Cargo.lock b/Cargo.lock index e709aa23..17e2beb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2863,6 +2863,7 @@ dependencies = [ "embassy-time", "embedded-io-async", "env_logger", + "fnv", "heapless", "libc", "log", diff --git a/sftp/out/cargo-expand-sftp.rs b/sftp/out/cargo-expand-sftp.rs deleted file mode 100644 index 0032029e..00000000 --- a/sftp/out/cargo-expand-sftp.rs +++ /dev/null @@ -1,1800 +0,0 @@ -#![feature(prelude_import)] -#[prelude_import] -use std::prelude::rust_2024::*; -#[macro_use] -extern crate std; -mod proto { - use num_enum::FromPrimitive; - use paste::paste; - use sunset::sshwire::{ - BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, - WireResult, - }; - use sunset_sshwire_derive::{SSHDecode, SSHEncode}; - #[allow(unused_imports)] - use log::{debug, error, info, log, trace, warn}; - pub struct Filename<'a>(TextString<'a>); - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Filename<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Filename", &&self.0) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Filename<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.0, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Filename<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) - } - } - pub struct FileHandle<'a>(pub BinString<'a>); - #[automatically_derived] - impl<'a> ::core::fmt::Debug for FileHandle<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_tuple_field1_finish(f, "FileHandle", &&self.0) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for FileHandle<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.0, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for FileHandle<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) - } - } - /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 - pub const SFTP_VERSION: u32 = 3; - /// The SFTP version of the client - pub struct InitVersionClient { - pub version: u32, - } - #[automatically_derived] - impl ::core::fmt::Debug for InitVersionClient { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "InitVersionClient", - "version", - &&self.version, - ) - } - } - impl ::sunset::sshwire::SSHEncode for InitVersionClient { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.version, s)?; - Ok(()) - } - } - impl<'de> ::sunset::sshwire::SSHDecode<'de> for InitVersionClient { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_version = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { version: field_version }) - } - } - /// The lowers SFTP version from the client and the server - pub struct InitVersionLowest { - pub version: u32, - } - #[automatically_derived] - impl ::core::fmt::Debug for InitVersionLowest { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "InitVersionLowest", - "version", - &&self.version, - ) - } - } - impl ::sunset::sshwire::SSHEncode for InitVersionLowest { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.version, s)?; - Ok(()) - } - } - impl<'de> ::sunset::sshwire::SSHDecode<'de> for InitVersionLowest { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_version = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { version: field_version }) - } - } - pub struct Open<'a> { - pub filename: Filename<'a>, - pub pflags: u32, - pub attrs: Attrs, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Open<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field3_finish( - f, - "Open", - "filename", - &self.filename, - "pflags", - &self.pflags, - "attrs", - &&self.attrs, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Open<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.filename, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.pflags, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.attrs, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Open<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_filename = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_pflags = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_attrs = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { - filename: field_filename, - pflags: field_pflags, - attrs: field_attrs, - }) - } - } - pub struct Close<'a> { - pub handle: FileHandle<'a>, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Close<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "Close", - "handle", - &&self.handle, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Close<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Close<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { handle: field_handle }) - } - } - pub struct Read<'a> { - pub handle: FileHandle<'a>, - pub offset: u64, - pub len: u32, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Read<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field3_finish( - f, - "Read", - "handle", - &self.handle, - "offset", - &self.offset, - "len", - &&self.len, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Read<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.offset, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.len, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Read<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_offset = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_len = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { - handle: field_handle, - offset: field_offset, - len: field_len, - }) - } - } - pub struct Write<'a> { - pub handle: FileHandle<'a>, - pub offset: u64, - pub data: BinString<'a>, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Write<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field3_finish( - f, - "Write", - "handle", - &self.handle, - "offset", - &self.offset, - "data", - &&self.data, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Write<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.offset, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.data, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Write<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_offset = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_data = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { - handle: field_handle, - offset: field_offset, - data: field_data, - }) - } - } - pub struct PathInfo<'a> { - pub path: TextString<'a>, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for PathInfo<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "PathInfo", - "path", - &&self.path, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for PathInfo<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.path, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for PathInfo<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_path = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { path: field_path }) - } - } - pub struct Status<'a> { - pub code: StatusCode, - pub message: TextString<'a>, - pub lang: TextString<'a>, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Status<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field3_finish( - f, - "Status", - "code", - &self.code, - "message", - &self.message, - "lang", - &&self.lang, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Status<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.code, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.message, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.lang, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Status<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_code = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_message = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_lang = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { - code: field_code, - message: field_message, - lang: field_lang, - }) - } - } - pub struct Handle<'a> { - pub handle: FileHandle<'a>, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Handle<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "Handle", - "handle", - &&self.handle, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Handle<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Handle<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { handle: field_handle }) - } - } - pub struct Data<'a> { - pub handle: FileHandle<'a>, - pub offset: u64, - pub data: BinString<'a>, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Data<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field3_finish( - f, - "Data", - "handle", - &self.handle, - "offset", - &self.offset, - "data", - &&self.data, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for Data<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.handle, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.offset, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.data, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for Data<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_handle = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_offset = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_data = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { - handle: field_handle, - offset: field_offset, - data: field_data, - }) - } - } - pub struct NameEntry<'a> { - pub filename: Filename<'a>, - /// longname is an undefined text line like "ls -l", - /// SHOULD NOT be used. - pub _longname: Filename<'a>, - pub attrs: Attrs, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for NameEntry<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field3_finish( - f, - "NameEntry", - "filename", - &self.filename, - "_longname", - &self._longname, - "attrs", - &&self.attrs, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for NameEntry<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.filename, s)?; - ::sunset::sshwire::SSHEncode::enc(&self._longname, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.attrs, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for NameEntry<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_filename = ::sunset::sshwire::SSHDecode::dec(s)?; - let field__longname = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_attrs = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { - filename: field_filename, - _longname: field__longname, - attrs: field_attrs, - }) - } - } - pub struct Name<'a>(pub Vec>); - #[automatically_derived] - impl<'a> ::core::fmt::Debug for Name<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Name", &&self.0) - } - } - impl<'a: 'de, 'de> SSHDecode<'de> for Name<'a> - where - 'de: 'a, - { - fn dec(s: &mut S) -> WireResult - where - S: SSHSource<'de>, - { - let count = u32::dec(s)? as usize; - let mut names = Vec::with_capacity(count); - for _ in 0..count { - names.push(NameEntry::dec(s)?); - } - Ok(Name(names)) - } - } - impl<'a> SSHEncode for Name<'a> { - fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { - (self.0.len() as u32).enc(s)?; - for element in self.0.iter() { - element.enc(s)?; - } - Ok(()) - } - } - pub struct ResponseAttributes { - pub attrs: Attrs, - } - #[automatically_derived] - impl ::core::fmt::Debug for ResponseAttributes { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field1_finish( - f, - "ResponseAttributes", - "attrs", - &&self.attrs, - ) - } - } - impl ::sunset::sshwire::SSHEncode for ResponseAttributes { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.attrs, s)?; - Ok(()) - } - } - impl<'de> ::sunset::sshwire::SSHDecode<'de> for ResponseAttributes { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_attrs = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { attrs: field_attrs }) - } - } - pub struct ReqId(pub u32); - #[automatically_derived] - impl ::core::fmt::Debug for ReqId { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_tuple_field1_finish(f, "ReqId", &&self.0) - } - } - impl ::sunset::sshwire::SSHEncode for ReqId { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.0, s)?; - Ok(()) - } - } - impl<'de> ::sunset::sshwire::SSHDecode<'de> for ReqId { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - Ok(Self(::sunset::sshwire::SSHDecode::dec(s)?)) - } - } - #[automatically_derived] - impl ::core::clone::Clone for ReqId { - #[inline] - fn clone(&self) -> ReqId { - let _: ::core::clone::AssertParamIsClone; - *self - } - } - #[automatically_derived] - impl ::core::marker::Copy for ReqId {} - #[repr(u32)] - #[allow(non_camel_case_types)] - pub enum StatusCode { - #[sshwire(variant = "ssh_fx_ok")] - SSH_FX_OK = 0, - #[sshwire(variant = "ssh_fx_eof")] - SSH_FX_EOF = 1, - #[sshwire(variant = "ssh_fx_no_such_file")] - SSH_FX_NO_SUCH_FILE = 2, - #[sshwire(variant = "ssh_fx_permission_denied")] - SSH_FX_PERMISSION_DENIED = 3, - #[sshwire(variant = "ssh_fx_failure")] - SSH_FX_FAILURE = 4, - #[sshwire(variant = "ssh_fx_bad_message")] - SSH_FX_BAD_MESSAGE = 5, - #[sshwire(variant = "ssh_fx_no_connection")] - SSH_FX_NO_CONNECTION = 6, - #[sshwire(variant = "ssh_fx_connection_lost")] - SSH_FX_CONNECTION_LOST = 7, - #[sshwire(variant = "ssh_fx_unsupported")] - SSH_FX_OP_UNSUPPORTED = 8, - #[sshwire(unknown)] - #[num_enum(catch_all)] - Other(u32), - } - #[automatically_derived] - #[allow(non_camel_case_types)] - impl ::core::fmt::Debug for StatusCode { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - match self { - StatusCode::SSH_FX_OK => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_OK") - } - StatusCode::SSH_FX_EOF => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_EOF") - } - StatusCode::SSH_FX_NO_SUCH_FILE => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_NO_SUCH_FILE") - } - StatusCode::SSH_FX_PERMISSION_DENIED => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_PERMISSION_DENIED") - } - StatusCode::SSH_FX_FAILURE => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_FAILURE") - } - StatusCode::SSH_FX_BAD_MESSAGE => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_BAD_MESSAGE") - } - StatusCode::SSH_FX_NO_CONNECTION => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_NO_CONNECTION") - } - StatusCode::SSH_FX_CONNECTION_LOST => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_CONNECTION_LOST") - } - StatusCode::SSH_FX_OP_UNSUPPORTED => { - ::core::fmt::Formatter::write_str(f, "SSH_FX_OP_UNSUPPORTED") - } - StatusCode::Other(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( - f, - "Other", - &__self_0, - ) - } - } - } - } - impl ::num_enum::FromPrimitive for StatusCode { - type Primitive = u32; - fn from_primitive(number: Self::Primitive) -> Self { - #![allow(non_upper_case_globals)] - const SSH_FX_OK__num_enum_0__: u32 = 0; - const SSH_FX_EOF__num_enum_0__: u32 = 1; - const SSH_FX_NO_SUCH_FILE__num_enum_0__: u32 = 2; - const SSH_FX_PERMISSION_DENIED__num_enum_0__: u32 = 3; - const SSH_FX_FAILURE__num_enum_0__: u32 = 4; - const SSH_FX_BAD_MESSAGE__num_enum_0__: u32 = 5; - const SSH_FX_NO_CONNECTION__num_enum_0__: u32 = 6; - const SSH_FX_CONNECTION_LOST__num_enum_0__: u32 = 7; - const SSH_FX_OP_UNSUPPORTED__num_enum_0__: u32 = 8; - #[deny(unreachable_patterns)] - match number { - SSH_FX_OK__num_enum_0__ => Self::SSH_FX_OK, - SSH_FX_EOF__num_enum_0__ => Self::SSH_FX_EOF, - SSH_FX_NO_SUCH_FILE__num_enum_0__ => Self::SSH_FX_NO_SUCH_FILE, - SSH_FX_PERMISSION_DENIED__num_enum_0__ => Self::SSH_FX_PERMISSION_DENIED, - SSH_FX_FAILURE__num_enum_0__ => Self::SSH_FX_FAILURE, - SSH_FX_BAD_MESSAGE__num_enum_0__ => Self::SSH_FX_BAD_MESSAGE, - SSH_FX_NO_CONNECTION__num_enum_0__ => Self::SSH_FX_NO_CONNECTION, - SSH_FX_CONNECTION_LOST__num_enum_0__ => Self::SSH_FX_CONNECTION_LOST, - SSH_FX_OP_UNSUPPORTED__num_enum_0__ => Self::SSH_FX_OP_UNSUPPORTED, - #[allow(unreachable_patterns)] - _ => Self::Other(number), - } - } - } - impl ::core::convert::From for StatusCode { - #[inline] - fn from(number: u32) -> Self { - ::num_enum::FromPrimitive::from_primitive(number) - } - } - #[doc(hidden)] - impl ::num_enum::CannotDeriveBothFromPrimitiveAndTryFromPrimitive for StatusCode {} - impl ::sunset::sshwire::SSHEncode for StatusCode { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - match *self { - Self::SSH_FX_OK => {} - Self::SSH_FX_EOF => {} - Self::SSH_FX_NO_SUCH_FILE => {} - Self::SSH_FX_PERMISSION_DENIED => {} - Self::SSH_FX_FAILURE => {} - Self::SSH_FX_BAD_MESSAGE => {} - Self::SSH_FX_NO_CONNECTION => {} - Self::SSH_FX_CONNECTION_LOST => {} - Self::SSH_FX_OP_UNSUPPORTED => {} - Self::Other(ref i) => { - return Err(::sunset::sshwire::WireError::UnknownVariant); - } - } - #[allow(unreachable_code)] Ok(()) - } - } - impl ::sunset::sshwire::SSHEncodeEnum for StatusCode { - fn variant_name(&self) -> ::sunset::sshwire::WireResult<&'static str> { - let r = match self { - Self::SSH_FX_OK => "ssh_fx_ok", - Self::SSH_FX_EOF => "ssh_fx_eof", - Self::SSH_FX_NO_SUCH_FILE => "ssh_fx_no_such_file", - Self::SSH_FX_PERMISSION_DENIED => "ssh_fx_permission_denied", - Self::SSH_FX_FAILURE => "ssh_fx_failure", - Self::SSH_FX_BAD_MESSAGE => "ssh_fx_bad_message", - Self::SSH_FX_NO_CONNECTION => "ssh_fx_no_connection", - Self::SSH_FX_CONNECTION_LOST => "ssh_fx_connection_lost", - Self::SSH_FX_OP_UNSUPPORTED => "ssh_fx_unsupported", - Self::Other(_) => { - return Err(::sunset::sshwire::WireError::UnknownVariant); - } - }; - #[allow(unreachable_code)] Ok(r) - } - } - impl<'de> SSHDecode<'de> for StatusCode { - fn dec(s: &mut S) -> WireResult - where - S: SSHSource<'de>, - { - Ok(StatusCode::from(u32::dec(s)?)) - } - } - pub struct ExtPair<'a> { - pub name: &'a str, - pub data: BinString<'a>, - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for ExtPair<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field2_finish( - f, - "ExtPair", - "name", - &self.name, - "data", - &&self.data, - ) - } - } - impl<'a> ::sunset::sshwire::SSHEncode for ExtPair<'a> { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - ::sunset::sshwire::SSHEncode::enc(&self.name, s)?; - ::sunset::sshwire::SSHEncode::enc(&self.data, s)?; - Ok(()) - } - } - impl<'de, 'a> ::sunset::sshwire::SSHDecode<'de> for ExtPair<'a> - where - 'de: 'a, - { - fn dec>( - s: &mut S, - ) -> ::sunset::sshwire::WireResult { - let field_name = ::sunset::sshwire::SSHDecode::dec(s)?; - let field_data = ::sunset::sshwire::SSHDecode::dec(s)?; - Ok(Self { - name: field_name, - data: field_data, - }) - } - } - /// Files attributes to describe Files as SFTP v3 specification - pub struct Attrs { - pub size: Option, - pub uid: Option, - pub gid: Option, - pub permissions: Option, - pub atime: Option, - pub mtime: Option, - pub ext_count: Option, - } - #[automatically_derived] - impl ::core::fmt::Debug for Attrs { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - let names: &'static _ = &[ - "size", - "uid", - "gid", - "permissions", - "atime", - "mtime", - "ext_count", - ]; - let values: &[&dyn ::core::fmt::Debug] = &[ - &self.size, - &self.uid, - &self.gid, - &self.permissions, - &self.atime, - &self.mtime, - &&self.ext_count, - ]; - ::core::fmt::Formatter::debug_struct_fields_finish(f, "Attrs", names, values) - } - } - #[automatically_derived] - impl ::core::default::Default for Attrs { - #[inline] - fn default() -> Attrs { - Attrs { - size: ::core::default::Default::default(), - uid: ::core::default::Default::default(), - gid: ::core::default::Default::default(), - permissions: ::core::default::Default::default(), - atime: ::core::default::Default::default(), - mtime: ::core::default::Default::default(), - ext_count: ::core::default::Default::default(), - } - } - } - #[repr(u32)] - #[allow(non_camel_case_types)] - pub enum AttrsFlags { - SSH_FILEXFER_ATTR_SIZE = 0x01, - SSH_FILEXFER_ATTR_UIDGID = 0x02, - SSH_FILEXFER_ATTR_PERMISSIONS = 0x04, - SSH_FILEXFER_ATTR_ACMODTIME = 0x08, - SSH_FILEXFER_ATTR_EXTENDED = 0x80000000, - } - impl core::ops::AddAssign for u32 { - fn add_assign(&mut self, other: AttrsFlags) { - *self |= other as u32; - } - } - impl core::ops::BitAnd for u32 { - type Output = u32; - fn bitand(self, rhs: AttrsFlags) -> Self::Output { - self & rhs as u32 - } - } - impl Attrs { - pub fn flags(&self) -> u32 { - let mut flags: u32 = 0; - if self.size.is_some() { - flags += AttrsFlags::SSH_FILEXFER_ATTR_SIZE; - } - if self.uid.is_some() || self.gid.is_some() { - flags += AttrsFlags::SSH_FILEXFER_ATTR_UIDGID; - } - if self.permissions.is_some() { - flags += AttrsFlags::SSH_FILEXFER_ATTR_PERMISSIONS; - } - if self.atime.is_some() || self.mtime.is_some() { - flags += AttrsFlags::SSH_FILEXFER_ATTR_ACMODTIME; - } - flags - } - } - impl SSHEncode for Attrs { - fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { - self.flags().enc(s)?; - if let Some(value) = self.size.as_ref() { - value.enc(s)? - } - if let Some(value) = self.uid.as_ref() { - value.enc(s)? - } - if let Some(value) = self.gid.as_ref() { - value.enc(s)? - } - if let Some(value) = self.permissions.as_ref() { - value.enc(s)? - } - if let Some(value) = self.atime.as_ref() { - value.enc(s)? - } - if let Some(value) = self.mtime.as_ref() { - value.enc(s)? - } - Ok(()) - } - } - impl<'de> SSHDecode<'de> for Attrs { - fn dec(s: &mut S) -> WireResult - where - S: SSHSource<'de>, - { - let mut attrs = Attrs::default(); - let flags = u32::dec(s)? as u32; - if flags & AttrsFlags::SSH_FILEXFER_ATTR_SIZE != 0 { - attrs.size = Some(u64::dec(s)?); - } - if flags & AttrsFlags::SSH_FILEXFER_ATTR_UIDGID != 0 { - attrs.uid = Some(u32::dec(s)?); - attrs.gid = Some(u32::dec(s)?); - } - if flags & AttrsFlags::SSH_FILEXFER_ATTR_PERMISSIONS != 0 { - attrs.permissions = Some(u32::dec(s)?); - } - if flags & AttrsFlags::SSH_FILEXFER_ATTR_ACMODTIME != 0 { - attrs.atime = Some(u32::dec(s)?); - attrs.mtime = Some(u32::dec(s)?); - } - Ok(attrs) - } - } - /// Represent a subset of the SFTP packet types defined by draft-ietf-secsh-filexfer-02 - #[repr(u8)] - #[allow(non_camel_case_types)] - pub enum SftpNum { - #[sshwire(variant = "ssh_fxp_init")] - SSH_FXP_INIT = 1, - #[sshwire(variant = "ssh_fxp_version")] - SSH_FXP_VERSION = 2, - #[sshwire(variant = "ssh_fxp_open")] - SSH_FXP_OPEN = 3, - #[sshwire(variant = "ssh_fxp_close")] - SSH_FXP_CLOSE = 4, - #[sshwire(variant = "ssh_fxp_read")] - SSH_FXP_READ = 5, - #[sshwire(variant = "ssh_fxp_write")] - SSH_FXP_WRITE = 6, - #[sshwire(variant = "ssh_fxp_realpath")] - SSH_FXP_REALPATH = 16, - #[sshwire(variant = "ssh_fxp_status")] - SSH_FXP_STATUS = 101, - #[sshwire(variant = "ssh_fxp_handle")] - SSH_FXP_HANDLE = 102, - #[sshwire(variant = "ssh_fxp_data")] - SSH_FXP_DATA = 103, - #[sshwire(variant = "ssh_fxp_name")] - SSH_FXP_NAME = 104, - #[sshwire(unknown)] - #[num_enum(catch_all)] - Other(u8), - } - #[automatically_derived] - #[allow(non_camel_case_types)] - impl ::core::fmt::Debug for SftpNum { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - match self { - SftpNum::SSH_FXP_INIT => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_INIT") - } - SftpNum::SSH_FXP_VERSION => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_VERSION") - } - SftpNum::SSH_FXP_OPEN => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_OPEN") - } - SftpNum::SSH_FXP_CLOSE => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_CLOSE") - } - SftpNum::SSH_FXP_READ => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_READ") - } - SftpNum::SSH_FXP_WRITE => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_WRITE") - } - SftpNum::SSH_FXP_REALPATH => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_REALPATH") - } - SftpNum::SSH_FXP_STATUS => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_STATUS") - } - SftpNum::SSH_FXP_HANDLE => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_HANDLE") - } - SftpNum::SSH_FXP_DATA => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_DATA") - } - SftpNum::SSH_FXP_NAME => { - ::core::fmt::Formatter::write_str(f, "SSH_FXP_NAME") - } - SftpNum::Other(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( - f, - "Other", - &__self_0, - ) - } - } - } - } - #[automatically_derived] - #[allow(non_camel_case_types)] - impl ::core::clone::Clone for SftpNum { - #[inline] - fn clone(&self) -> SftpNum { - match self { - SftpNum::SSH_FXP_INIT => SftpNum::SSH_FXP_INIT, - SftpNum::SSH_FXP_VERSION => SftpNum::SSH_FXP_VERSION, - SftpNum::SSH_FXP_OPEN => SftpNum::SSH_FXP_OPEN, - SftpNum::SSH_FXP_CLOSE => SftpNum::SSH_FXP_CLOSE, - SftpNum::SSH_FXP_READ => SftpNum::SSH_FXP_READ, - SftpNum::SSH_FXP_WRITE => SftpNum::SSH_FXP_WRITE, - SftpNum::SSH_FXP_REALPATH => SftpNum::SSH_FXP_REALPATH, - SftpNum::SSH_FXP_STATUS => SftpNum::SSH_FXP_STATUS, - SftpNum::SSH_FXP_HANDLE => SftpNum::SSH_FXP_HANDLE, - SftpNum::SSH_FXP_DATA => SftpNum::SSH_FXP_DATA, - SftpNum::SSH_FXP_NAME => SftpNum::SSH_FXP_NAME, - SftpNum::Other(__self_0) => { - SftpNum::Other(::core::clone::Clone::clone(__self_0)) - } - } - } - } - impl ::num_enum::FromPrimitive for SftpNum { - type Primitive = u8; - fn from_primitive(number: Self::Primitive) -> Self { - #![allow(non_upper_case_globals)] - const SSH_FXP_INIT__num_enum_0__: u8 = 1; - const SSH_FXP_VERSION__num_enum_0__: u8 = 2; - const SSH_FXP_OPEN__num_enum_0__: u8 = 3; - const SSH_FXP_CLOSE__num_enum_0__: u8 = 4; - const SSH_FXP_READ__num_enum_0__: u8 = 5; - const SSH_FXP_WRITE__num_enum_0__: u8 = 6; - const SSH_FXP_REALPATH__num_enum_0__: u8 = 16; - const SSH_FXP_STATUS__num_enum_0__: u8 = 101; - const SSH_FXP_HANDLE__num_enum_0__: u8 = 102; - const SSH_FXP_DATA__num_enum_0__: u8 = 103; - const SSH_FXP_NAME__num_enum_0__: u8 = 104; - #[deny(unreachable_patterns)] - match number { - SSH_FXP_INIT__num_enum_0__ => Self::SSH_FXP_INIT, - SSH_FXP_VERSION__num_enum_0__ => Self::SSH_FXP_VERSION, - SSH_FXP_OPEN__num_enum_0__ => Self::SSH_FXP_OPEN, - SSH_FXP_CLOSE__num_enum_0__ => Self::SSH_FXP_CLOSE, - SSH_FXP_READ__num_enum_0__ => Self::SSH_FXP_READ, - SSH_FXP_WRITE__num_enum_0__ => Self::SSH_FXP_WRITE, - SSH_FXP_REALPATH__num_enum_0__ => Self::SSH_FXP_REALPATH, - SSH_FXP_STATUS__num_enum_0__ => Self::SSH_FXP_STATUS, - SSH_FXP_HANDLE__num_enum_0__ => Self::SSH_FXP_HANDLE, - SSH_FXP_DATA__num_enum_0__ => Self::SSH_FXP_DATA, - SSH_FXP_NAME__num_enum_0__ => Self::SSH_FXP_NAME, - #[allow(unreachable_patterns)] - _ => Self::Other(number), - } - } - } - impl ::core::convert::From for SftpNum { - #[inline] - fn from(number: u8) -> Self { - ::num_enum::FromPrimitive::from_primitive(number) - } - } - #[doc(hidden)] - impl ::num_enum::CannotDeriveBothFromPrimitiveAndTryFromPrimitive for SftpNum {} - impl ::sunset::sshwire::SSHEncode for SftpNum { - fn enc( - &self, - s: &mut dyn ::sunset::sshwire::SSHSink, - ) -> ::sunset::sshwire::WireResult<()> { - match *self { - Self::SSH_FXP_INIT => {} - Self::SSH_FXP_VERSION => {} - Self::SSH_FXP_OPEN => {} - Self::SSH_FXP_CLOSE => {} - Self::SSH_FXP_READ => {} - Self::SSH_FXP_WRITE => {} - Self::SSH_FXP_REALPATH => {} - Self::SSH_FXP_STATUS => {} - Self::SSH_FXP_HANDLE => {} - Self::SSH_FXP_DATA => {} - Self::SSH_FXP_NAME => {} - Self::Other(ref i) => { - return Err(::sunset::sshwire::WireError::UnknownVariant); - } - } - #[allow(unreachable_code)] Ok(()) - } - } - impl ::sunset::sshwire::SSHEncodeEnum for SftpNum { - fn variant_name(&self) -> ::sunset::sshwire::WireResult<&'static str> { - let r = match self { - Self::SSH_FXP_INIT => "ssh_fxp_init", - Self::SSH_FXP_VERSION => "ssh_fxp_version", - Self::SSH_FXP_OPEN => "ssh_fxp_open", - Self::SSH_FXP_CLOSE => "ssh_fxp_close", - Self::SSH_FXP_READ => "ssh_fxp_read", - Self::SSH_FXP_WRITE => "ssh_fxp_write", - Self::SSH_FXP_REALPATH => "ssh_fxp_realpath", - Self::SSH_FXP_STATUS => "ssh_fxp_status", - Self::SSH_FXP_HANDLE => "ssh_fxp_handle", - Self::SSH_FXP_DATA => "ssh_fxp_data", - Self::SSH_FXP_NAME => "ssh_fxp_name", - Self::Other(_) => { - return Err(::sunset::sshwire::WireError::UnknownVariant); - } - }; - #[allow(unreachable_code)] Ok(r) - } - } - impl<'de> SSHDecode<'de> for SftpNum { - fn dec(s: &mut S) -> WireResult - where - S: SSHSource<'de>, - { - Ok(SftpNum::from(u8::dec(s)?)) - } - } - impl From for u8 { - fn from(sftp_num: SftpNum) -> u8 { - match sftp_num { - SftpNum::SSH_FXP_INIT => 1, - SftpNum::SSH_FXP_VERSION => 2, - SftpNum::SSH_FXP_OPEN => 3, - SftpNum::SSH_FXP_CLOSE => 4, - SftpNum::SSH_FXP_READ => 5, - SftpNum::SSH_FXP_WRITE => 6, - SftpNum::SSH_FXP_REALPATH => 16, - SftpNum::SSH_FXP_STATUS => 101, - SftpNum::SSH_FXP_HANDLE => 102, - SftpNum::SSH_FXP_DATA => 103, - SftpNum::SSH_FXP_NAME => 104, - SftpNum::Other(number) => number, - } - } - } - impl SftpNum { - fn is_init(&self) -> bool { - (1..=1).contains(&(u8::from(self.clone()))) - } - fn is_request(&self) -> bool { - (2..=99).contains(&(u8::from(self.clone()))) - } - fn is_response(&self) -> bool { - (100..=199).contains(&(u8::from(self.clone()))) - } - } - /// Top level SSH packet enum - /// - /// It helps identifying the SFTP Packet type and handling it accordingly - /// This is done using the SFTP field type - pub enum SftpPacket<'a> { - Init(InitVersionClient), - Version(InitVersionLowest), - Open(ReqId, Open<'a>), - Close(ReqId, Close<'a>), - Read(ReqId, Read<'a>), - Write(ReqId, Write<'a>), - PathInfo(ReqId, PathInfo<'a>), - Status(ReqId, Status<'a>), - Handle(ReqId, Handle<'a>), - Data(ReqId, Data<'a>), - Name(ReqId, Name<'a>), - } - #[automatically_derived] - impl<'a> ::core::fmt::Debug for SftpPacket<'a> { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - match self { - SftpPacket::Init(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( - f, - "Init", - &__self_0, - ) - } - SftpPacket::Version(__self_0) => { - ::core::fmt::Formatter::debug_tuple_field1_finish( - f, - "Version", - &__self_0, - ) - } - SftpPacket::Open(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Open", - __self_0, - &__self_1, - ) - } - SftpPacket::Close(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Close", - __self_0, - &__self_1, - ) - } - SftpPacket::Read(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Read", - __self_0, - &__self_1, - ) - } - SftpPacket::Write(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Write", - __self_0, - &__self_1, - ) - } - SftpPacket::PathInfo(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "PathInfo", - __self_0, - &__self_1, - ) - } - SftpPacket::Status(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Status", - __self_0, - &__self_1, - ) - } - SftpPacket::Handle(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Handle", - __self_0, - &__self_1, - ) - } - SftpPacket::Data(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Data", - __self_0, - &__self_1, - ) - } - SftpPacket::Name(__self_0, __self_1) => { - ::core::fmt::Formatter::debug_tuple_field2_finish( - f, - "Name", - __self_0, - &__self_1, - ) - } - } - } - } - impl SSHEncode for SftpPacket<'_> { - fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { - let t = u8::from(self.sftp_num()); - t.enc(s)?; - match self { - SftpPacket::Init(p) => p.enc(s)?, - SftpPacket::Version(p) => p.enc(s)?, - SftpPacket::Open(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::Close(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::Read(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::Write(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::PathInfo(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::Status(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::Handle(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::Data(id, p) => { - id.enc(s)?; - p.enc(s)? - } - SftpPacket::Name(id, p) => { - id.enc(s)?; - p.enc(s)? - } - }; - Ok(()) - } - } - impl<'a: 'de, 'de> SSHDecode<'de> for SftpPacket<'a> - where - 'de: 'a, - { - fn dec(s: &mut S) -> WireResult - where - S: SSHSource<'de>, - { - let packet_type_number = u8::dec(s)?; - let packet_type = SftpNum::from(packet_type_number); - let decoded_packet = match packet_type { - SftpNum::SSH_FXP_INIT => { - let inner_type = ::dec(s)?; - SftpPacket::Init(inner_type) - } - SftpNum::SSH_FXP_VERSION => { - let inner_type = ::dec(s)?; - SftpPacket::Version(inner_type) - } - SftpNum::SSH_FXP_OPEN => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Open(req_id, inner_type) - } - SftpNum::SSH_FXP_CLOSE => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Close(req_id, inner_type) - } - SftpNum::SSH_FXP_READ => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Read(req_id, inner_type) - } - SftpNum::SSH_FXP_WRITE => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Write(req_id, inner_type) - } - SftpNum::SSH_FXP_REALPATH => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::PathInfo(req_id, inner_type) - } - SftpNum::SSH_FXP_STATUS => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Status(req_id, inner_type) - } - SftpNum::SSH_FXP_HANDLE => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Handle(req_id, inner_type) - } - SftpNum::SSH_FXP_DATA => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Data(req_id, inner_type) - } - SftpNum::SSH_FXP_NAME => { - let req_id = ::dec(s)?; - let inner_type = >::dec(s)?; - SftpPacket::Name(req_id, inner_type) - } - _ => { - return Err(WireError::UnknownPacket { - number: packet_type_number, - }); - } - }; - Ok(decoded_packet) - } - } - impl<'a> SftpPacket<'a> { - /// Maps `SpecificPacketVariant` to `message_num` - pub fn sftp_num(&self) -> SftpNum { - match self { - SftpPacket::Init(_) => SftpNum::from(1 as u8), - SftpPacket::Version(_) => SftpNum::from(2 as u8), - SftpPacket::Open(_, _) => SftpNum::from(3 as u8), - SftpPacket::Close(_, _) => SftpNum::from(4 as u8), - SftpPacket::Read(_, _) => SftpNum::from(5 as u8), - SftpPacket::Write(_, _) => SftpNum::from(6 as u8), - SftpPacket::PathInfo(_, _) => SftpNum::from(16 as u8), - SftpPacket::Status(_, _) => SftpNum::from(101 as u8), - SftpPacket::Handle(_, _) => SftpNum::from(102 as u8), - SftpPacket::Data(_, _) => SftpNum::from(103 as u8), - SftpPacket::Name(_, _) => SftpNum::from(104 as u8), - } - } - /// Encode a request. - /// - /// Used by a SFTP client. Does not include the length field. - pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { - if !self.sftp_num().is_request() { - return Err(WireError::PacketWrong); - } - self.sftp_num().enc(s)?; - id.0.enc(s)?; - self.enc(s) - } - /// Decode a response. - /// - /// Used by a SFTP client. Does not include the length field. - pub fn decode_response<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> - where - S: SSHSource<'de>, - 'a: 'de, - 'de: 'a, - { - let num = SftpNum::from(u8::dec(s)?); - if !num.is_response() { - return Err(WireError::PacketWrong); - } - let id = ReqId(u32::dec(s)?); - Ok((id, Self::dec(s)?)) - } - /// Decode a request. Includes Init - /// - /// Used by a SFTP server. Does not include the length field. If the request does not have id (Initialisation) - pub fn decode_request<'de, S>(s: &mut S) -> WireResult<(Option, Self)> - where - S: SSHSource<'de>, - 'a: 'de, - 'de: 'a, - { - let num = SftpNum::from(u8::dec(s)?); - if (!num.is_request() && !num.is_init()) { - return Err(WireError::PacketWrong); - } - let maybe_id = if num.is_init() { None } else { Some(ReqId(u32::dec(s)?)) }; - let sftp_packet = Self::dec(s)?; - Ok((maybe_id, sftp_packet)) - } - /// Encode a response. - /// - /// Used by a SFTP server. Does not include the length field. - pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> WireResult<()> { - if !self.sftp_num().is_response() { - return Err(WireError::PacketWrong); - } - self.sftp_num().enc(s)?; - id.0.enc(s)?; - self.enc(s) - } - } - impl<'a> From for SftpPacket<'a> { - fn from(s: InitVersionClient) -> SftpPacket<'a> { - SftpPacket::Init(s) - } - } - impl<'a> From for SftpPacket<'a> { - fn from(s: InitVersionLowest) -> SftpPacket<'a> { - SftpPacket::Version(s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Open<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_open", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Open(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Close<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_close", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Close(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Read<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_read", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Read(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Write<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_write", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Write(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: PathInfo<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_realpath", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::PathInfo(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Status<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_status", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Status(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Handle<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_handle", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Handle(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Data<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_data", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Data(ReqId(0), s) - } - } - /// **Warning**: No Sequence Id can be infered from a Packet Type - impl<'a> From> for SftpPacket<'a> { - fn from(s: Name<'a>) -> SftpPacket<'a> { - { - { - let lvl = ::log::Level::Warn; - if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() { - ::log::__private_api::log( - { ::log::__private_api::GlobalLogger }, - format_args!( - "Casting from {0:?} to SftpPacket cannot set Request Id", - "ssh_fxp_name", - ), - lvl, - &( - "sunset_sftp::proto", - "sunset_sftp::proto", - ::log::__private_api::loc(), - ), - (), - ); - } - } - }; - SftpPacket::Name(ReqId(0), s) - } - } -} -mod sftpserver { - use crate::proto::{Attrs, StatusCode}; - use core::marker::PhantomData; - pub type Result = core::result::Result; - /// All trait functions are optional in the SFTP protocol. - /// Some less core operations have a Provided implementation returning - /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, - /// but may return `Err(StatusCode::SSH_FX_OP_UNSUPPORTED)`. - pub trait SftpServer { - type Handle; - async fn open(filename: &str, flags: u32, attrs: &Attrs) -> Result; - /// Close either a file or directory handle - async fn close(handle: &Self::Handle) -> Result<()>; - async fn read( - handle: &Self::Handle, - offset: u64, - reply: &mut ReadReply, - ) -> Result<()>; - async fn write(handle: &Self::Handle, offset: u64, buf: &[u8]) -> Result<()>; - async fn opendir(dir: &str) -> Result; - async fn readdir(handle: &Self::Handle, reply: &mut DirReply) -> Result<()>; - } - pub struct ReadReply<'g, 'a> { - chan: ChanOut<'g, 'a>, - } - impl<'g, 'a> ReadReply<'g, 'a> { - pub async fn reply(self, data: &[u8]) {} - } - pub struct DirReply<'g, 'a> { - chan: ChanOut<'g, 'a>, - } - impl<'g, 'a> DirReply<'g, 'a> { - pub async fn reply(self, data: &[u8]) {} - } - pub struct ChanOut<'g, 'a> { - _phantom_g: PhantomData<&'g ()>, - _phantom_a: PhantomData<&'a ()>, - } -} -pub use sftpserver::DirReply; -pub use sftpserver::ReadReply; -pub use sftpserver::Result; -pub use sftpserver::SftpServer; -pub use proto::Attrs; -pub use proto::SFTP_VERSION; -pub use proto::SftpNum; -pub use proto::SftpPacket; From 8549e01f2d0c8f64249ac07066c939e10770eb9b Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 23 Sep 2025 20:08:56 +1000 Subject: [PATCH 293/393] Bact to SftpHandle: Process failing as soon as possible on short packets --- sftp/src/sftphandle.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 0d2357c0..43619ce7 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -82,6 +82,12 @@ where let in_len = buffer_in.len(); debug!("Received {:} bytes to process", in_len); + if self.partial_write_request_tracker.is_none() + & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) + { + return Err(WireError::PacketWrong); + } + let mut source = SftpSource::new(buffer_in); trace!("Source content: {:?}", source); @@ -151,12 +157,6 @@ where return Ok(sink.finalize()); } - if self.partial_write_request_tracker.is_none() - & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) - { - return Err(WireError::PacketWrong); - } - let packet_length = u32::dec(&mut source)?; trace!("Packet field length content: {}", packet_length); From 4c30d0400ccb7b85eca3351d03e9ed4361174595 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 24 Sep 2025 09:35:05 +1000 Subject: [PATCH 294/393] Added extra docs to SFTP proto decode_request --- sftp/src/proto.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 01abe820..b5bae2e2 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -609,7 +609,7 @@ macro_rules! sftpmessages { /// /// Used by a SFTP server. Does not include the length field. /// - /// It will fail if the received packet is a response + /// It will fail if the received packet is a response, no valid or incomplete packet pub fn decode_request<'de, S>(s: &mut S) -> WireResult where S: SSHSource<'de>, @@ -617,7 +617,6 @@ macro_rules! sftpmessages { 'de: 'a { - // let sftp_packet = Self::dec(s)?; match Self::dec(s) { Ok(sftp_packet)=> { if (!sftp_packet.sftp_num().is_request() From 9179c233088cee7c98a6d7438e8aefdcb486cf08 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 24 Sep 2025 20:11:55 +1000 Subject: [PATCH 295/393] Broken: Ugly experimentation with multi part consecutive packets Somehow I am sending an ok different from the expected by the client --- sftp/src/proto.rs | 3 +- sftp/src/sftphandle.rs | 242 ++++++++++++++++++++--------------------- 2 files changed, 121 insertions(+), 124 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index b5bae2e2..5a709e42 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -616,7 +616,8 @@ macro_rules! sftpmessages { 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes 'de: 'a { - + let packet_length = u32::dec(s)?; + debug!("Packet field len = {:?}. Source len = {:?}", packet_length, s.remaining()); match Self::dec(s) { Ok(sftp_packet)=> { if (!sftp_packet.sftp_num().is_request() diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 43619ce7..c7949ee2 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,13 +1,13 @@ use crate::OpaqueFileHandle; use crate::proto::{ self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, - SftpPacket, Status, StatusCode, + SftpPacket, Status, StatusCode, Write, }; use crate::sftpserver::SftpServer; use crate::sftpsink::SftpSink; use crate::sftpsource::SftpSource; -use sunset::sshwire::{SSHDecode, WireError, WireResult}; +use sunset::sshwire::{WireError, WireResult}; use core::u32; #[allow(unused_imports)] @@ -38,7 +38,7 @@ impl PartialWriteRequestTracker { }) } - pub fn get_file_handle(&self) -> T { + pub fn get_opaque_file_handle(&self) -> T { self.obscure_file_handle.clone() } } @@ -81,7 +81,6 @@ where ) -> WireResult { let in_len = buffer_in.len(); debug!("Received {:} bytes to process", in_len); - if self.partial_write_request_tracker.is_none() & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) { @@ -94,149 +93,146 @@ where let mut sink = SftpSink::new(buffer_out); if let Some(mut write_tracker) = self.partial_write_request_tracker.take() { - trace!( + debug!( "Processing successive chunks of a long write packet. Stored data: {:?}", write_tracker ); + let opaque_handle = write_tracker.get_opaque_file_handle(); + + let partial_write = { + let usable_data = in_len.min(write_tracker.remain_data_len as usize); + + if in_len > write_tracker.remain_data_len as usize { + // This is because we are receiving one packet and the beginning of the next one + // let sftp_num = source.peak_packet_type_with_offset( + // write_tracker.remain_data_len as usize, + // )?; + // error!( + // "There is too much data in the buffer! {:?} > than expected {:?}. There is a trailing packet in buffer: {:?}", + // in_len, write_tracker.remain_data_len, sftp_num + // ); + }; - let usable_data = in_len.min(write_tracker.remain_data_len as usize); + let data_segment = source.dec_as_binstring(usable_data)?; - if in_len > write_tracker.remain_data_len as usize { - // This is because we are receiving one packet and the beginning of the next one - let sftp_num = source.peak_packet_type_with_offset( - write_tracker.remain_data_len as usize, - )?; - error!( - "There is too much data in the buffer! {:?} > than expected {:?}. There is a trailing packet in buffer: {:?}", - in_len, write_tracker.remain_data_len, sftp_num - ); - }; + // TODO: Do proper casting with checks u32::try_from(data_in_buffer.0.len()) + let data_segment_len = data_segment.0.len() as u32; - let data_segment = source.dec_as_binstring(usable_data)?; + let current_write_offset = write_tracker.remain_data_offset; + write_tracker.remain_data_offset += data_segment_len as u64; + write_tracker.remain_data_len -= data_segment_len; - // TODO: Do proper casting with checks u32::try_from(data_in_buffer.0.len()) - let data_segment_len = data_segment.0.len() as u32; + let obscure_file_handle = write_tracker.get_opaque_file_handle(); + debug!( + "Processing successive chunks of a long write packet. Writing : obscure_file_handle = {:?}, write_offset = {:?}, data_segment = {:?}, data remaining = {:?}", + obscure_file_handle, + current_write_offset, + data_segment, + write_tracker.remain_data_len + ); - let current_write_offset = write_tracker.remain_data_offset; - write_tracker.remain_data_offset += data_segment_len as u64; - write_tracker.remain_data_len -= data_segment_len; + let part_sftp_packet_write_request = SftpPacket::Write( + write_tracker.req_id, + Write { + handle: opaque_handle.into_file_handle(), + offset: current_write_offset, + data: data_segment, + }, + ); - let obscure_file_handle = write_tracker.get_file_handle(); + part_sftp_packet_write_request + }; + + self.process_known_request(&mut sink, partial_write).await?; debug!( - "Processing successive chunks of a long write packet. Writing : obscure_file_handle = {:?}, write_offset = {:?}, data_segment = {:?}, data remaining = {:?}", - obscure_file_handle, - current_write_offset, - data_segment, - write_tracker.remain_data_len + "Finishing partial_write process. write_tracker = {:?}", + write_tracker ); - match self.file_server.write( - &obscure_file_handle, - current_write_offset, - data_segment.as_ref(), - ) { - Ok(_) => { - if write_tracker.remain_data_len > 0 { - self.partial_write_request_tracker = Some(write_tracker); - } else { - push_ok(write_tracker.req_id, &mut sink)?; - info!("Finished multi part Write Request"); - self.partial_write_request_tracker = None; // redundant - } + if write_tracker.remain_data_len > 0 { + self.partial_write_request_tracker = Some(write_tracker); + } else { + push_ok(write_tracker.req_id, &mut sink)?; + info!("Finished multi part Write Request"); + self.partial_write_request_tracker = None; // redundant + } + } else { + match SftpPacket::decode_request(&mut source) { + Ok(request) => { + info!("received request: {:?}", request); + self.process_known_request(&mut sink, request).await?; } Err(e) => { - self.partial_write_request_tracker = None; - error!("SFTP write thrown: {:?}", e); - push_general_failure( - write_tracker.req_id, - "error writing", - &mut sink, - )?; - } - }; - - return Ok(sink.finalize()); - } - - let packet_length = u32::dec(&mut source)?; - trace!("Packet field length content: {}", packet_length); - - match SftpPacket::decode_request(&mut source) { - Ok(request) => { - info!("received request: {:?}", request); - self.process_known_request(&mut sink, request).await?; - } - Err(e) => { - match e { - WireError::RanOut => { - warn!( - "RanOut for the SFTP Packet in the source buffer: {:?}", - e - ); - // let packet_total_length = source.peak_packet_len()?; - let packet_type = source.peak_packet_type()?; - match packet_type { - SftpNum::SSH_FXP_WRITE => { - let ( + match e { + WireError::RanOut => { + warn!( + "RanOut for the SFTP Packet in the source buffer: {:?}", + e + ); + // let packet_total_length = source.peak_packet_len()?; + let packet_type = source.peak_packet_type()?; + match packet_type { + SftpNum::SSH_FXP_WRITE => { + let ( file_handle, req_id, offset, data_in_buffer, write_tracker, ) = source - .get_packet_partial_write_content_and_tracker( - )?; - debug!( - "Packet is too long for the source buffer, will write what we have now and continue writing later" - ); - trace!( - "handle = {:?}, req_id = {:?}, offset = {:?}, data_in_buffer = {:?}, write_tracker = {:?}", - file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle - req_id, - offset, - data_in_buffer, - write_tracker - ); - - match self.file_server.write( - &file_handle, - offset, - data_in_buffer.as_ref(), - ) { - Ok(_) => { - self.partial_write_request_tracker = - Some(write_tracker); - } - Err(e) => { - error!("SFTP write thrown: {:?}", e); - push_general_failure( - req_id, - "error writing ", - &mut sink, - )?; - } - }; - } - _ => { - push_general_failure( - ReqId(u32::MAX), - "Unsupported Request: Too long", - &mut sink, + .get_packet_partial_write_content_and_tracker( )?; - } - }; - } - WireError::UnknownPacket { number: _ } => { - warn!("Error decoding SFTP Packet:{:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; - } - _ => { - error!("Error decoding SFTP Packet: {:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; + debug!( + "Packet is too long for the source buffer, will write what we have now and continue writing later" + ); + trace!( + "handle = {:?}, req_id = {:?}, offset = {:?}, data_in_buffer = {:?}, write_tracker = {:?}", + file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle + req_id, + offset, + data_in_buffer, + write_tracker + ); + + match self.file_server.write( + &file_handle, + offset, + data_in_buffer.as_ref(), + ) { + Ok(_) => { + self.partial_write_request_tracker = + Some(write_tracker); + } + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure( + req_id, + "error writing ", + &mut sink, + )?; + } + }; + } + _ => { + push_general_failure( + ReqId(u32::MAX), + "Unsupported Request: Too long", + &mut sink, + )?; + } + }; + } + WireError::UnknownPacket { number: _ } => { + warn!("Error decoding SFTP Packet:{:?}", e); + push_unsupported(ReqId(u32::MAX), &mut sink)?; + } + _ => { + error!("Error decoding SFTP Packet: {:?}", e); + push_unsupported(ReqId(u32::MAX), &mut sink)?; + } } } } }; - Ok(sink.finalize()) } From 4d6f6aa50051b08db5bfa935c74c5aa926af8818 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 24 Sep 2025 20:27:10 +1000 Subject: [PATCH 296/393] decoding request len in SftpPacket::decode_request --- sftp/src/proto.rs | 3 ++- sftp/src/sftphandle.rs | 18 ++++-------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 5a709e42..0ed4bea0 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -617,7 +617,8 @@ macro_rules! sftpmessages { 'de: 'a { let packet_length = u32::dec(s)?; - debug!("Packet field len = {:?}. Source len = {:?}", packet_length, s.remaining()); + trace!("Packet field len = {:?}, buffer len = {:?}", packet_length, s.remaining()); + match Self::dec(s) { Ok(sftp_packet)=> { if (!sftp_packet.sftp_num().is_request() diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index c7949ee2..c22c6750 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -140,20 +140,10 @@ where }, ); - part_sftp_packet_write_request - }; - - self.process_known_request(&mut sink, partial_write).await?; - debug!( - "Finishing partial_write process. write_tracker = {:?}", - write_tracker - ); - if write_tracker.remain_data_len > 0 { - self.partial_write_request_tracker = Some(write_tracker); - } else { - push_ok(write_tracker.req_id, &mut sink)?; - info!("Finished multi part Write Request"); - self.partial_write_request_tracker = None; // redundant + match SftpPacket::decode_request(&mut source) { + Ok(request) => { + info!("received request: {:?}", request); + self.process_known_request(&mut sink, request).await?; } } else { match SftpPacket::decode_request(&mut source) { From 329575cba403a078a6353c0fa73ddecccd54b771 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 25 Sep 2025 13:41:07 +1000 Subject: [PATCH 297/393] Handing consecutive request in a buffer with some problems The issue that is left to fix is what happen when the second request is trunked and cannot be decoded. I am contemplating creating a "truncated" request buffer that would be used when the request cannot be decoded because requires extra data to be received in the next process call. Apart from this, the current code is able to process fragmented and consecutive SSH_FXP_WRITE for files with occasional problems caused by the issue detailed. --- demo/sftp/std/testing/test_write_requests.sh | 5 +- sftp/src/opaquefilehandle.rs | 1 + sftp/src/sftphandle.rs | 264 ++++++++++--------- sftp/src/sftpsource.rs | 98 +++---- 4 files changed, 189 insertions(+), 179 deletions(-) diff --git a/demo/sftp/std/testing/test_write_requests.sh b/demo/sftp/std/testing/test_write_requests.sh index dc615c1a..8ac5d5e7 100755 --- a/demo/sftp/std/testing/test_write_requests.sh +++ b/demo/sftp/std/testing/test_write_requests.sh @@ -5,7 +5,7 @@ REMOTE_HOST="192.168.69.2" REMOTE_USER="any" # Define test files -FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random") +FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random" "256kB_random" "1MB_random" "2MB_random") # Generate random data files echo "Generating random data files..." @@ -13,6 +13,9 @@ dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null dd if=/dev/random bs=1024 count=65 of=./65kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=256 of=./256kB_random 2>/dev/null +dd if=/dev/random bs=1048576 count=1 of=./1MB_random 2>/dev/null +dd if=/dev/random bs=1048576 count=2 of=./2MB_random 2>/dev/null echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." diff --git a/sftp/src/opaquefilehandle.rs b/sftp/src/opaquefilehandle.rs index 2bdf739b..d96114a6 100644 --- a/sftp/src/opaquefilehandle.rs +++ b/sftp/src/opaquefilehandle.rs @@ -3,6 +3,7 @@ use crate::FileHandle; use sunset::sshwire::WireResult; /// This is the trait with the required methods for interoperability between different opaque file handles +/// used in SFTP transactions pub trait OpaqueFileHandle: Sized + Clone + core::hash::Hash + PartialEq + Eq + core::fmt::Debug { diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index c22c6750..cca7c9f3 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,13 +1,13 @@ use crate::OpaqueFileHandle; use crate::proto::{ self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, - SftpPacket, Status, StatusCode, Write, + SftpPacket, Status, StatusCode, }; use crate::sftpserver::SftpServer; use crate::sftpsink::SftpSink; use crate::sftpsource::SftpSource; -use sunset::sshwire::{WireError, WireResult}; +use sunset::sshwire::{SSHSource, WireError, WireResult}; use core::u32; #[allow(unused_imports)] @@ -43,7 +43,9 @@ impl PartialWriteRequestTracker { } } -//#[derive(Debug, Clone)] +/// Process the raw buffers in and out from a subsystem channel decoding request and encoding responses +/// +/// It will delegate request to an `SftpServer` implemented by the library user taking into account the local system details. pub struct SftpHandler<'a, T, S> where T: OpaqueFileHandle, @@ -80,39 +82,37 @@ where buffer_out: &mut [u8], ) -> WireResult { let in_len = buffer_in.len(); - debug!("Received {:} bytes to process", in_len); + let mut buffer_in_remaining_index = 0; + + let out_len = buffer_out.len(); + let mut used_out_accumulated_index = 0; + + trace!("Received {:} bytes to process", in_len); if self.partial_write_request_tracker.is_none() & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) { return Err(WireError::PacketWrong); } - let mut source = SftpSource::new(buffer_in); - trace!("Source content: {:?}", source); + while buffer_in_remaining_index < in_len { + let mut source = + SftpSource::new(&buffer_in[buffer_in_remaining_index..]); + trace!("Source content: {:?}", source); - let mut sink = SftpSink::new(buffer_out); + let mut sink = + SftpSink::new(&mut buffer_out[used_out_accumulated_index..]); - if let Some(mut write_tracker) = self.partial_write_request_tracker.take() { - debug!( - "Processing successive chunks of a long write packet. Stored data: {:?}", - write_tracker - ); - let opaque_handle = write_tracker.get_opaque_file_handle(); + if let Some(mut write_tracker) = + self.partial_write_request_tracker.take() + { + trace!( + "Processing successive chunks of a long write packet. Stored data: {:?}", + write_tracker + ); + let opaque_handle = write_tracker.get_opaque_file_handle(); - let partial_write = { let usable_data = in_len.min(write_tracker.remain_data_len as usize); - if in_len > write_tracker.remain_data_len as usize { - // This is because we are receiving one packet and the beginning of the next one - // let sftp_num = source.peak_packet_type_with_offset( - // write_tracker.remain_data_len as usize, - // )?; - // error!( - // "There is too much data in the buffer! {:?} > than expected {:?}. There is a trailing packet in buffer: {:?}", - // in_len, write_tracker.remain_data_len, sftp_num - // ); - }; - let data_segment = source.dec_as_binstring(usable_data)?; // TODO: Do proper casting with checks u32::try_from(data_in_buffer.0.len()) @@ -122,108 +122,136 @@ where write_tracker.remain_data_offset += data_segment_len as u64; write_tracker.remain_data_len -= data_segment_len; - let obscure_file_handle = write_tracker.get_opaque_file_handle(); - debug!( - "Processing successive chunks of a long write packet. Writing : obscure_file_handle = {:?}, write_offset = {:?}, data_segment = {:?}, data remaining = {:?}", - obscure_file_handle, + trace!( + "Processing successive chunks of a long write packet. Writing : opaque_handle = {:?}, write_offset = {:?}, data_segment = {:?}, data remaining = {:?}", + opaque_handle, current_write_offset, data_segment, write_tracker.remain_data_len ); - let part_sftp_packet_write_request = SftpPacket::Write( - write_tracker.req_id, - Write { - handle: opaque_handle.into_file_handle(), - offset: current_write_offset, - data: data_segment, - }, - ); - - match SftpPacket::decode_request(&mut source) { - Ok(request) => { - info!("received request: {:?}", request); - self.process_known_request(&mut sink, request).await?; - } - } else { - match SftpPacket::decode_request(&mut source) { - Ok(request) => { - info!("received request: {:?}", request); - self.process_known_request(&mut sink, request).await?; - } - Err(e) => { - match e { - WireError::RanOut => { - warn!( - "RanOut for the SFTP Packet in the source buffer: {:?}", - e - ); - // let packet_total_length = source.peak_packet_len()?; - let packet_type = source.peak_packet_type()?; - match packet_type { - SftpNum::SSH_FXP_WRITE => { - let ( - file_handle, - req_id, - offset, - data_in_buffer, - write_tracker, - ) = source - .get_packet_partial_write_content_and_tracker( - )?; - debug!( - "Packet is too long for the source buffer, will write what we have now and continue writing later" - ); - trace!( - "handle = {:?}, req_id = {:?}, offset = {:?}, data_in_buffer = {:?}, write_tracker = {:?}", - file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle - req_id, - offset, - data_in_buffer, - write_tracker - ); - - match self.file_server.write( - &file_handle, - offset, - data_in_buffer.as_ref(), - ) { - Ok(_) => { - self.partial_write_request_tracker = - Some(write_tracker); - } - Err(e) => { - error!("SFTP write thrown: {:?}", e); - push_general_failure( - req_id, - "error writing ", - &mut sink, - )?; - } - }; - } - _ => { - push_general_failure( - ReqId(u32::MAX), - "Unsupported Request: Too long", - &mut sink, - )?; - } - }; - } - WireError::UnknownPacket { number: _ } => { - warn!("Error decoding SFTP Packet:{:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; + match self.file_server.write( + &opaque_handle, + current_write_offset, + data_segment.as_ref(), + ) { + Ok(_) => { + if write_tracker.remain_data_len > 0 { + self.partial_write_request_tracker = Some(write_tracker); + } else { + push_ok(write_tracker.req_id, &mut sink)?; + info!("Finished multi part Write Request"); } - _ => { - error!("Error decoding SFTP Packet: {:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; + } + Err(e) => { + self.partial_write_request_tracker = None; + error!("SFTP write thrown: {:?}", e); + push_general_failure( + write_tracker.req_id, + "error writing", + &mut sink, + )?; + } + }; + } else { + match SftpPacket::decode_request(&mut source) { + Ok(request) => { + info!("received request: {:?}", request); + self.process_known_request(&mut sink, request).await?; + } + Err(e) => { + match e { + WireError::RanOut => { + warn!( + "RanOut for the SFTP Packet in the source buffer: {:?}", + e + ); + + let packet_type = source.peak_packet_type()?; + match packet_type { + SftpNum::SSH_FXP_WRITE => { + debug!( + "about to decode packet partial write content", + ); + let ( + file_handle, + req_id, + offset, + data_in_buffer, + write_tracker, + ) = source.dec_packet_partial_write_content_and_get_tracker()?; + + trace!( + "handle = {:?}, req_id = {:?}, offset = {:?}, data_in_buffer = {:?}, write_tracker = {:?}", + file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle + req_id, + offset, + data_in_buffer, + write_tracker + ); + + match self.file_server.write( + &file_handle, + offset, + data_in_buffer.as_ref(), + ) { + Ok(_) => { + self.partial_write_request_tracker = + Some(write_tracker); + } + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure( + req_id, + "error writing ", + &mut sink, + )?; + } + }; + } + _ => { + push_general_failure( + ReqId(u32::MAX), + "Unsupported Request: Too long", + &mut sink, + )?; + } + }; + } + WireError::UnknownPacket { number: _ } => { + warn!("Error decoding SFTP Packet:{:?}", e); + push_unsupported(ReqId(u32::MAX), &mut sink)?; + } + _ => { + error!("Error decoding SFTP Packet: {:?}", e); + push_unsupported(ReqId(u32::MAX), &mut sink)?; + } } } } + }; + + // We will use these indexes to create new source and sink to process extra requests in the buffer. + buffer_in_remaining_index = in_len - source.remaining(); + if source.remaining() > 0 { + debug!( + "After processing request: Source bytes remaining = {:?}, buffer_in len = {:?} => buffer_in_remaining_index = {:?}", + source.remaining(), + in_len, + buffer_in_remaining_index + ); + trace!("Source dump: {:?}", source); + debug!( + "Buffer in left to process: {:?}", + &buffer_in[buffer_in_remaining_index..] + ); } - }; - Ok(sink.finalize()) + + // TODO: What about buffer_out overflow condition? + used_out_accumulated_index += sink.finalize(); + } + + Ok(used_out_accumulated_index) } async fn process_known_request( @@ -318,7 +346,7 @@ fn push_ok(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireError> { lang: "EN".into(), }, ); - debug!("Pushing an OK status message: {:?}", response); + trace!("Pushing an OK status message: {:?}", response); response.encode_response(sink)?; Ok(()) } diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 9c97cc85..7a8a4be2 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -60,78 +60,56 @@ impl<'de> SftpSource<'de> { } } - /// Peaks the buffer for packet type adding an offset. This does not advance the reading index - /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail - /// - /// **Warning**: This might only work in special conditions, such as those where the , in other case the result will contain garbage - pub(crate) fn peak_packet_type_with_offset( - &self, - starting_offset: usize, - ) -> WireResult { - // const SFTP_ID_BUFFER_INDEX: usize = 4; // All SFTP packet have the packet type after a u32 length field - // const SFTP_MINIMUM_LENGTH: usize = 9; // Corresponds to a minimal SSH_FXP_INIT packet - if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - Err(WireError::PacketWrong) - } else { - Ok(SftpNum::from(self.buffer[starting_offset + SFTP_FIELD_ID_INDEX])) - } - } - - /// Assuming that the buffer contains a Write request packet initial bytes, Peaks the buffer for the handle length. This does not advance the reading index - /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// Assuming that the buffer contains a Write request packet initial bytes and not its totality, + /// extracts a partial version of the write request and a Write request tracker to handle and a + /// tracker to continue processing subsequent portions of the request from a SftpSource /// /// **Warning**: will only work in well formed write packets, in other case the result will contain garbage - pub(crate) fn get_packet_partial_write_content_and_tracker< + pub(crate) fn dec_packet_partial_write_content_and_get_tracker< T: OpaqueFileHandle, >( &mut self, ) -> WireResult<(T, ReqId, u64, BinString<'de>, PartialWriteRequestTracker)> { if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - Err(WireError::PacketWrong) - } else { - let prev_index = self.index; - self.index = SFTP_WRITE_REQID_INDEX; - let req_id = ReqId::dec(self)?; - let file_handle = FileHandle::dec(self)?; - - let obscured_file_handle = OpaqueFileHandle::try_from(&file_handle)?; - let offset = u64::dec(self)?; - let data_len = u32::dec(self)?; - - let data_len_in_buffer = self.buffer.len() - self.index; - let data_in_buffer = BinString(self.take(data_len_in_buffer)?); - - self.index = prev_index; - - let remain_data_len = data_len - data_len_in_buffer as u32; - let remain_data_offset = offset + data_len_in_buffer as u64; - trace!( - "Request ID = {:?}, Handle = {:?}, offset = {:?}, data length in buffer = {:?}, data in current buffer {:?} ", - req_id, file_handle, offset, data_len_in_buffer, data_in_buffer - ); - - let write_tracker = PartialWriteRequestTracker::new( - req_id, - OpaqueFileHandle::try_from(&file_handle)?, - remain_data_len, - remain_data_offset, - )?; - - Ok((obscured_file_handle, req_id, offset, data_in_buffer, write_tracker)) + return Err(WireError::PacketWrong); // TODO: Find a better error } - } - /// Used to decode the whole SSHSource as a single BinString ignoring the len field - /// - /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. - pub(crate) fn dec_all_as_binstring(&mut self) -> WireResult> { - Ok(BinString(self.take(self.buffer.len())?)) + match self.peak_packet_type()? { + SftpNum::SSH_FXP_WRITE => {} + _ => return Err(WireError::PacketWrong), // TODO: Find a better error + }; + + let prev_index = self.index; + self.index = SFTP_WRITE_REQID_INDEX; + let req_id = ReqId::dec(self)?; + let file_handle = FileHandle::dec(self)?; + + let obscured_file_handle = OpaqueFileHandle::try_from(&file_handle)?; + let offset = u64::dec(self)?; + let data_len = u32::dec(self)?; + + let data_len_in_buffer = self.buffer.len() - self.index; + let data_in_buffer = BinString(self.take(data_len_in_buffer)?); + + let remain_data_len = data_len - data_len_in_buffer as u32; + let remain_data_offset = offset + data_len_in_buffer as u64; + trace!( + "Request ID = {:?}, Handle = {:?}, offset = {:?}, data length in buffer = {:?}, data in current buffer {:?} ", + req_id, file_handle, offset, data_len_in_buffer, data_in_buffer + ); + + let write_tracker = PartialWriteRequestTracker::new( + req_id, + OpaqueFileHandle::try_from(&file_handle)?, + remain_data_len, + remain_data_offset, + )?; + + Ok((obscured_file_handle, req_id, offset, data_in_buffer, write_tracker)) } - /// Used to decode a slice of SSHSource as a single BinString ignoring the len field + /// Used to decode a slice of SSHSource as a single BinString /// /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. pub(crate) fn dec_as_binstring( From d1bca6fdaaae67f4fe38f57cac36fdefb021549a Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 25 Sep 2025 14:34:47 +1000 Subject: [PATCH 298/393] WIP: Modifying an error condition to RanOut and adding peak_packet_len --- sftp/src/sftpsource.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 7a8a4be2..1a6c1bb2 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -54,12 +54,30 @@ impl<'de> SftpSource<'de> { /// **Warning**: will only work in well formed packets, in other case the result will contain garbage pub(crate) fn peak_packet_type(&self) -> WireResult { if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - Err(WireError::PacketWrong) + Err(WireError::RanOut) } else { Ok(SftpNum::from(self.buffer[SFTP_FIELD_ID_INDEX])) } } + /// Peaks the buffer for packet length. This does not advance the reading index + /// + /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// + /// **Warning**: will only work in well formed packets, in other case the result will contain garbage + pub(crate) fn peak_packet_len(&self) -> WireResult { + if self.buffer.len() < SFTP_FIELD_LEN_INDEX + SFTP_FIELD_LEN_LENGTH { + Err(WireError::RanOut) + } else { + let bytes: [u8; 4] = self.buffer + [SFTP_FIELD_LEN_INDEX..SFTP_FIELD_LEN_INDEX + SFTP_FIELD_LEN_LENGTH] + .try_into() + .expect("slice length mismatch"); + + Ok(u32::from_be_bytes(bytes)) + } + } + /// Assuming that the buffer contains a Write request packet initial bytes and not its totality, /// extracts a partial version of the write request and a Write request tracker to handle and a /// tracker to continue processing subsequent portions of the request from a SftpSource @@ -118,4 +136,8 @@ impl<'de> SftpSource<'de> { ) -> WireResult> { Ok(BinString(self.take(len)?)) } + + pub(crate) fn buffer_ref(&self) -> &[u8] { + self.buffer.clone() + } } From 8f2ed5b6c43f43218c895ea8b6101b1cc9ff27e9 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 2 Oct 2025 08:15:51 +1000 Subject: [PATCH 299/393] WIP: Restructuring SftpHandle. Adding a FSM Not finished, but this structure prevents borrowing issues --- demo/sftp/std/src/main.rs | 19 +- sftp/src/lib.rs | 7 + sftp/src/requestholder.rs | 288 ++++++++++++++++++ sftp/src/sftperror.rs | 51 +++- sftp/src/sftphandle.rs | 597 +++++++++++++++++++++++++++++--------- sftp/src/sftpsource.rs | 7 +- 6 files changed, 822 insertions(+), 147 deletions(-) create mode 100644 sftp/src/requestholder.rs diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 63be11e3..b70e9046 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -1,14 +1,13 @@ use sunset::*; use sunset_async::{ProgressHolder, SSHServer, SunsetMutex, SunsetRawMutex}; -use sunset_sftp::SftpHandler; +use sunset_sftp::{RequestHolder, SftpHandler}; pub(crate) use sunset_demo_common as demo_common; use demo_common::{DemoCommon, DemoServer, SSHConfig}; use crate::{ - demoopaquefilehandle::DemoOpaqueFileHandle, - demosftpserver::{DemoSftpServer, PrivateFileHandler}, + demoopaquefilehandle::DemoOpaqueFileHandle, demosftpserver::DemoSftpServer, }; use embedded_io_async::{Read, Write}; @@ -148,8 +147,13 @@ impl DemoServer for StdDemo { info!("SFTP loop has received a channel handle {:?}", ch.num()); + // TODO: Do some research to find reasonable default buffer lengths let mut buffer_in = [0u8; 512]; - let mut buffer_out = [0u8; 512]; + let mut buffer_out = [0u8; 384]; + let mut incomplete_request_buffer = [0u8; 128]; // TODO: Find a non arbitrary length + + let mut incomplete_request_holder = + RequestHolder::new(&mut incomplete_request_buffer); match { let mut stdio = serv.stdio(ch).await?; @@ -160,6 +164,7 @@ impl DemoServer for StdDemo { let mut sftp_handler = SftpHandler::::new( &mut file_server, + // &mut incomplete_request_buffer, ); loop { let lr = stdio.read(&mut buffer_in).await?; @@ -170,7 +175,11 @@ impl DemoServer for StdDemo { } let lw = sftp_handler - .process(&buffer_in[0..lr], &mut buffer_out) + .process( + &buffer_in[0..lr], + &mut incomplete_request_holder, + &mut buffer_out, + ) .await?; if lw > 0 { stdio.write(&mut buffer_out[0..lw]).await?; diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 0b9278a1..ee2019f4 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,19 +1,26 @@ mod opaquefilehandle; mod proto; +mod requestholder; mod sftphandle; mod sftpserver; mod sftpsink; mod sftpsource; +mod sftperror; + pub use sftpserver::DirReply; pub use sftpserver::ReadReply; pub use sftpserver::SftpOpResult; pub use sftpserver::SftpServer; +pub use requestholder::RequestHolder; + pub use sftphandle::SftpHandler; pub use opaquefilehandle::{OpaqueFileHandle, OpaqueFileHandleManager, PathFinder}; +pub use sftperror::{SftpError, SftpResult}; + pub use proto::Attrs; pub use proto::FileHandle; pub use proto::Filename; diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs new file mode 100644 index 00000000..7b6df9b7 --- /dev/null +++ b/sftp/src/requestholder.rs @@ -0,0 +1,288 @@ +use crate::{proto, sftperror, sftpsource::SftpSource}; + +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; +use sunset::sshwire::WireError; + +#[derive(Debug)] +pub(crate) enum RequestHolderError { + /// The slice to hold is too long + NoRoom, + /// The slice holder is keeping a slice already. Consideer cleaning + Busy, + /// The slice holder is empty + Empty, + /// The instance has been invalidated + Invalid, + /// There is not enough data in the slice we are trying to add. we need more data + RanOut, + /// WireError + WireError(WireError), +} + +impl From for RequestHolderError { + fn from(value: WireError) -> Self { + RequestHolderError::WireError(value) + } +} + +pub(crate) type RequestHolderResult = Result; + +/// Helper struct to manage short fragmented requests that have been +/// received in consecutive read operations +/// +/// For requests exceeding the length of buffers other techniques, such +/// as composing them into multiple request, might help reducing the +/// required buffer sizes. This is recommended for restricted environments. +/// +/// The intended use for this RequestHolder is (in order): +/// - `new`: Initialize the struct with a slice that will keep the +/// request in memory +/// +/// - `try_hold`: load the data for an incomplete request +/// +/// - `try_append_for_valid_request`: append more data from another +/// slice to complete the request +/// +/// - `try_get_ref`: returns a reference to the portion of the slice +/// containing a request +/// +/// - `reset`: reset counters and flags to allow `try_hold` a new request +/// +/// - **OR** `invalidate`: return the reference to the slice provided in `new` +/// and mark the structure as invalid. At this point it should be disposed +#[derive(Debug)] +pub struct RequestHolder<'a> { + /// The buffer used to contain the data for the request + buffer: &'a mut [u8], + /// The index of the last byte in the buffer containing usable data + buffer_fill_index: usize, + /// Number of bytes appended to foundational `try_hold` slice + appended: usize, + /// Marks the structure as invalid + invalid: bool, + /// Used to mark when the structure is holding data + busy: bool, +} + +impl<'a> RequestHolder<'a> { + /// The buffer will be used to hold a full request. Choose a + /// reasonable size for this buffer. + pub fn new(buffer: &'a mut [u8]) -> Self { + RequestHolder { + buffer: buffer, + buffer_fill_index: 0, + invalid: false, + busy: false, + appended: 0, + } + } + + /// Resets the structure allowing it to hold a new request. + /// + /// Will not remove the previous data from the buffer + pub fn reset(&mut self) -> () { + self.busy = false; + self.invalid = false; + self.buffer_fill_index = 0; + self.appended = 0; + } + + /// Invalidates the current instance and returns its original buffer. Does not erase previous data + pub fn invalidate(&mut self) -> RequestHolderResult<&[u8]> { + if !self.busy { + return Err(RequestHolderError::Empty); + } + if self.invalid { + return Err(RequestHolderError::Invalid); + } + + self.invalid = true; + // self.buffer_fill_index = 0; + Ok(&self.buffer) + } + + /// Uses the internal buffer to store a copy of the provided slice + /// + /// The definition of `try_hold` and `try_append_slice` separately + /// is deliberated to follow an order in composing the held request + /// + /// returns the number of bytes read from the slice + pub fn try_hold(&mut self, slice: &[u8]) -> RequestHolderResult { + if self.busy { + return Err(RequestHolderError::Busy); + } + if self.invalid { + return Err(RequestHolderError::Invalid); + } + + self.busy = true; + self.try_append_slice(slice)?; + let read_in = self.appended(); + self.appended = 0; + Ok(read_in) + } + + /// Appends a slice to the internal buffer. Requires the buffer to + /// be busy by using `try_hold` first + /// + /// Increases the `appended` counter + /// + /// Returns the number of bytes appended + fn try_append_slice(&mut self, slice: &[u8]) -> RequestHolderResult<()> { + if slice.len() == 0 { + warn!("try appending a zero length slice"); + return Ok(()); + } + if !self.busy { + return Err(RequestHolderError::Empty); + } + + if self.invalid { + return Err(RequestHolderError::Invalid); + } + + let in_len = slice.len(); + if in_len > self.remaining_len() { + return Err(RequestHolderError::NoRoom); + } + debug!("Adding: {:?}", slice); + + self.buffer[self.buffer_fill_index..self.buffer_fill_index + in_len] + .copy_from_slice(slice); + + self.buffer_fill_index += in_len; + debug!( + "RequestHolder: index = {:?}, slice = {:?}", + self.buffer_fill_index, + self.try_get_ref()? + ); + self.appended += in_len; + Ok(()) + } + + /// Using the content of the `RequestHolder` tries to find a valid + /// SFTP request appending from slice into the internal buffer to + /// form a valid request + /// + /// Returns the number of bytes appended + pub fn try_append_for_valid_request( + &mut self, + slice: &[u8], + ) -> RequestHolderResult<()> { + self.appended = 0; // reset appended bytes counter + + debug!( + "try_append_for_valid_request: self = {:?}\n\ + Space left = {:?}\n\ + Length of slice to append from = {:?}", + self, + self.remaining_len(), + slice.len() + ); + + if self.invalid { + return Err(RequestHolderError::Invalid); + } + + if !self.busy { + return Err(RequestHolderError::Empty); + } + + // This makes sure that we do not try to read more slice than we can + if self.buffer_fill_index + slice.len() < proto::SFTP_FIELD_ID_INDEX { + self.try_append_slice(&slice)?; + return Err(RequestHolderError::RanOut); + } + + let complete_to_id_index = proto::SFTP_FIELD_ID_INDEX + .checked_sub(self.buffer_fill_index - 1) + .unwrap_or(0); + + if complete_to_id_index > 0 { + warn!( + "The held fragment len = {:?}, is insufficient to peak\ + the length and type. Will append {:?} to reach the \ + id field index: {:?}", + self.buffer_fill_index, + complete_to_id_index, + proto::SFTP_FIELD_ID_INDEX + ); + if complete_to_id_index > slice.len() { + self.try_append_slice(&slice)?; + error!( + "The slice to include to the held fragment is too \ + short to complete to id index. More data is required." + ); + return Err(RequestHolderError::RanOut); + } else { + self.try_append_slice(&slice[..complete_to_id_index])?; + }; + } + + let packet_len = + SftpSource::new(self.try_get_ref()?).peak_packet_len()? as usize; + + let packet_type = SftpSource::new(self.try_get_ref()?).peak_packet_type()?; + debug!("Request len = {:?}, type = {:?}", packet_len, packet_type); + + let remaining_packet_len = packet_len - self.buffer_fill_index; // TODO: Careful, the packet len does not include the packet len field + + if remaining_packet_len <= (self.remaining_len()) { + if remaining_packet_len > slice.len() { + self.try_append_slice(&slice[self.appended()..])?; + error!( + "The slice to include to the held fragment does not \ + contain the whole packet. More data is required." + ); + return Err(RequestHolderError::RanOut); + } + self.try_append_slice(&slice[self.appended()..])?; // The only Ok + } else { + warn!( + "The request does not fit in the buffer: \ + (req len = {:?} > buffer len = {:?} )", + packet_len, + self.buffer.len() + ); + if self.remaining_len() < (slice.len() - self.appended()) { + self.try_append_slice( + &slice[self.appended()..self.remaining_len()], + )?; + } else { + self.try_append_slice(&slice[self.appended()..])?; + } + return Err(RequestHolderError::NoRoom); + } + Ok(()) + } + + /// Gets a reference to the slice that it is holding + pub fn try_get_ref(&self) -> RequestHolderResult<&[u8]> { + if self.invalid { + return Err(RequestHolderError::Invalid); + } + + if self.busy { + Ok(&self.buffer[..self.buffer_fill_index]) + } else { + Err(RequestHolderError::Empty) + } + } + + /// Returns true if it has a slice in its buffer + pub fn is_busy(&self) -> bool { + self.busy + } + + /// Returns the bytes appened in the last call to `try_append_for_valid_request` + pub fn appended(&self) -> usize { + self.appended + } + + /// Returns the number of bytes unused at the end of the buffer, + /// this is, the remaining length + fn remaining_len(&self) -> usize { + self.buffer.len() - self.buffer_fill_index - 1 // Off by one? + } +} diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index b78393bb..98192a4d 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -1,11 +1,19 @@ use core::convert::From; +use sunset::Error as SunsetError; use sunset::sshwire::WireError; +use crate::{SftpOpResult, StatusCode, requestholder::RequestHolderError}; + #[derive(Debug)] pub enum SftpError { + NotInitialized, + AlreadyInitialized, + MalformedPacket, WireError(WireError), - // SshError(SshError), + OperationError(StatusCode), + SunsetError(SunsetError), + RequestHolderError(RequestHolderError), } impl From for SftpError { @@ -14,4 +22,45 @@ impl From for SftpError { } } +impl From for SftpError { + fn from(value: SunsetError) -> Self { + SftpError::SunsetError(value) + } +} + +impl From for SftpError { + fn from(value: StatusCode) -> Self { + SftpError::OperationError(value) + } +} + +impl From for SftpError { + fn from(value: RequestHolderError) -> Self { + SftpError::RequestHolderError(value) + } +} + +impl From for WireError { + fn from(value: SftpError) -> Self { + match value { + SftpError::WireError(wire_error) => wire_error, + _ => WireError::PacketWrong, + } + } +} + +impl From for SunsetError { + fn from(value: SftpError) -> Self { + match value { + SftpError::SunsetError(error) => error, + SftpError::WireError(wire_error) => wire_error.into(), + SftpError::NotInitialized => SunsetError::PacketWrong {}, + SftpError::AlreadyInitialized => SunsetError::PacketWrong {}, + SftpError::MalformedPacket => SunsetError::PacketWrong {}, + SftpError::OperationError(_) => SunsetError::PacketWrong {}, + SftpError::RequestHolderError(request_holder_error) => SunsetError::Bug, + } + } +} + pub type SftpResult = Result; diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index cca7c9f3..4eb3ead9 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,18 +1,40 @@ -use crate::OpaqueFileHandle; use crate::proto::{ - self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, - SftpPacket, Status, StatusCode, + self, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, SFTP_MINIMUM_PACKET_LEN, + SFTP_VERSION, SftpNum, SftpPacket, Status, StatusCode, }; +use crate::requestholder::{RequestHolder, RequestHolderError}; +use crate::sftperror::SftpResult; use crate::sftpserver::SftpServer; use crate::sftpsink::SftpSink; use crate::sftpsource::SftpSource; +use crate::{OpaqueFileHandle, SftpError}; +use sunset::Error as SunsetError; +use sunset::Error; use sunset::sshwire::{SSHSource, WireError, WireResult}; -use core::u32; +use core::{u32, usize}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -use std::usize; + +#[derive(Default, Debug, PartialEq, Eq)] +enum SftpHandleState { + /// The handle is not initialized + #[default] + Initializing, + /// The handle is ready to process requests + Idle, + /// The request is fragmented and needs special handling + Fragmented(FragmentedRequestState), +} + +#[derive(Debug, PartialEq, Eq)] +enum FragmentedRequestState { + /// A request that is clipped needs to be process. It cannot be decoded and more bytes are needed + ProcessingClippedRequest, + /// A request, with a length over the incoming buffer capacity is being processed + ProcessingLongRequest, +} /// Used to keep record of a long SFTP Write request that does not fit in receiving buffer and requires processing in batches #[derive(Debug)] @@ -51,6 +73,9 @@ where T: OpaqueFileHandle, S: SftpServer<'a, T>, { + /// Holds the internal state if the SFTP handle + state: SftpHandleState, + /// The local SFTP File server implementing the basic SFTP requests defined by `SftpServer` file_server: &'a mut S, @@ -71,202 +96,499 @@ where file_server, initialized: false, partial_write_request_tracker: None, + state: SftpHandleState::default(), } } /// Decodes the buffer_in request, process the request delegating operations to an Struct implementing SftpServer, - /// serializes an answer in buffer_out and return the length used in buffer_out + /// serializes an answer in buffer_out and **returns** the length used in buffer_out pub async fn process( &mut self, buffer_in: &[u8], + incomplete_request_holder: &mut RequestHolder<'_>, buffer_out: &mut [u8], - ) -> WireResult { + ) -> SftpResult { let in_len = buffer_in.len(); let mut buffer_in_remaining_index = 0; - let out_len = buffer_out.len(); let mut used_out_accumulated_index = 0; trace!("Received {:} bytes to process", in_len); - if self.partial_write_request_tracker.is_none() + + // let mut pending_incomplete_request = incomplete_request_holder.is_busy(); + // let pending_long_request = self.partial_write_request_tracker.is_some(); + + if !matches!(self.state, SftpHandleState::Fragmented(_)) & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) { - return Err(WireError::PacketWrong); + return Err(WireError::PacketWrong.into()); } while buffer_in_remaining_index < in_len { - let mut source = - SftpSource::new(&buffer_in[buffer_in_remaining_index..]); - trace!("Source content: {:?}", source); - let mut sink = SftpSink::new(&mut buffer_out[used_out_accumulated_index..]); - if let Some(mut write_tracker) = - self.partial_write_request_tracker.take() - { - trace!( - "Processing successive chunks of a long write packet. Stored data: {:?}", - write_tracker - ); - let opaque_handle = write_tracker.get_opaque_file_handle(); + debug!("SFTP Process State: {:?}", self.state); - let usable_data = in_len.min(write_tracker.remain_data_len as usize); + match &self.state { + SftpHandleState::Fragmented(fragment_case) => { + match fragment_case { + FragmentedRequestState::ProcessingClippedRequest => { + let append_result = incomplete_request_holder + .try_append_for_valid_request( + &buffer_in[buffer_in_remaining_index..], + ); - let data_segment = source.dec_as_binstring(usable_data)?; + if let Err(e) = append_result { + match e { + RequestHolderError::RanOut => { + warn!( + "There was not enough bytes in the buffer_in. We will continue adding bytes" + ); + buffer_in_remaining_index += + incomplete_request_holder.appended(); + continue; + } + RequestHolderError::NoRoom => { + todo!( + "This is an incomplete long packet situation. You hope that it is a Write request" + ) + } + _ => { + error!( + "Unhandled error completing incomplete request {:?}", + e, + ); + return Err(SunsetError::Bug.into()); + } + } + } + let mut source = SftpSource::new( + &incomplete_request_holder.try_get_ref()?, + ); + trace!("Internal Source Content: {:?}", source); - // TODO: Do proper casting with checks u32::try_from(data_in_buffer.0.len()) - let data_segment_len = data_segment.0.len() as u32; + let sftp_packet = + SftpPacket::decode_request(&mut source); - let current_write_offset = write_tracker.remain_data_offset; - write_tracker.remain_data_offset += data_segment_len as u64; - write_tracker.remain_data_len -= data_segment_len; + // TODO: Here we have to check the append_result or simply the sftp_packet to handle Errors such as TooLongRanOut or similar - trace!( - "Processing successive chunks of a long write packet. Writing : opaque_handle = {:?}, write_offset = {:?}, data_segment = {:?}, data remaining = {:?}", - opaque_handle, - current_write_offset, - data_segment, - write_tracker.remain_data_len - ); + let used = incomplete_request_holder.appended(); + buffer_in_remaining_index += used; - match self.file_server.write( - &opaque_handle, - current_write_offset, - data_segment.as_ref(), - ) { - Ok(_) => { - if write_tracker.remain_data_len > 0 { - self.partial_write_request_tracker = Some(write_tracker); - } else { - push_ok(write_tracker.req_id, &mut sink)?; - info!("Finished multi part Write Request"); + incomplete_request_holder.reset(); + + todo!("FragmentedRequestState::ProcessingClippedRequest") + } + FragmentedRequestState::ProcessingLongRequest => { + let mut source = SftpSource::new( + &buffer_in[buffer_in_remaining_index..], + ); + trace!("Source content: {:?}", source); + + let mut write_tracker = if let Some(wt) = + self.partial_write_request_tracker.take() + { + wt + } else { + return Err(SftpError::SunsetError( + SunsetError::Bug, + )); + }; + + let opaque_handle = + write_tracker.get_opaque_file_handle(); + + let usable_data = + in_len.min(write_tracker.remain_data_len as usize); + + let data_segment = // Fails!! + source.dec_as_binstring(usable_data)?; + + let data_segment_len = + u32::try_from(data_segment.0.len()) + .map_err(|e| SunsetError::Bug)?; + let current_write_offset = + write_tracker.remain_data_offset; + write_tracker.remain_data_offset += + data_segment_len as u64; + write_tracker.remain_data_len -= data_segment_len; + + debug!( + "Processing successive chunks of a long write packet. \ + Writing : opaque_handle = {:?}, write_offset = {:?}, \ + data_segment = {:?}, data remaining = {:?}", + opaque_handle, + current_write_offset, + data_segment, + write_tracker.remain_data_len + ); + + match self.file_server.write( + &opaque_handle, + current_write_offset, + data_segment.as_ref(), + ) { + Ok(_) => { + if write_tracker.remain_data_len > 0 { + self.partial_write_request_tracker = + Some(write_tracker); + } else { + push_ok(write_tracker.req_id, &mut sink)?; + info!("Finished multi part Write Request"); + self.state = SftpHandleState::Idle; + } + } + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure( + write_tracker.req_id, + "error writing", + &mut sink, + )?; + self.state = SftpHandleState::Idle; + } + }; + buffer_in_remaining_index = in_len - source.remaining(); } } - Err(e) => { - self.partial_write_request_tracker = None; - error!("SFTP write thrown: {:?}", e); - push_general_failure( - write_tracker.req_id, - "error writing", - &mut sink, - )?; - } - }; - } else { - match SftpPacket::decode_request(&mut source) { - Ok(request) => { - info!("received request: {:?}", request); - self.process_known_request(&mut sink, request).await?; - } - Err(e) => { - match e { - WireError::RanOut => { - warn!( - "RanOut for the SFTP Packet in the source buffer: {:?}", - e - ); + } - let packet_type = source.peak_packet_type()?; - match packet_type { - SftpNum::SSH_FXP_WRITE => { - debug!( - "about to decode packet partial write content", - ); - let ( - file_handle, - req_id, - offset, - data_in_buffer, - write_tracker, - ) = source.dec_packet_partial_write_content_and_get_tracker()?; - - trace!( - "handle = {:?}, req_id = {:?}, offset = {:?}, data_in_buffer = {:?}, write_tracker = {:?}", - file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle - req_id, - offset, - data_in_buffer, - write_tracker + _ => { + let mut source = + SftpSource::new(&buffer_in[buffer_in_remaining_index..]); + trace!("Source content: {:?}", source); + + let sftp_packet = SftpPacket::decode_request(&mut source); + + match self.state { + SftpHandleState::Fragmented(_) => { + return Err( + SftpError::SunsetError(SunsetError::Bug).into() + ); + } + SftpHandleState::Initializing => match sftp_packet { + Ok(request) => { + match request { + SftpPacket::Init(init_version_client) => { + let version = + SftpPacket::Version(InitVersionLowest { + version: SFTP_VERSION, + }); + + info!("Sending '{:?}'", version); + version.encode_response(&mut sink)?; + self.state = SftpHandleState::Idle; + } + _ => { + error!( + "Request received before init: {:?}", + request ); + return Err(SftpError::NotInitialized); + } + }; + } + Err(_) => { + error!( + "Malformed SFTP Packet before Init: {:?}", + sftp_packet + ); + return Err(SftpError::MalformedPacket); + } + }, + SftpHandleState::Idle => { + match sftp_packet { + Ok(request) => match request { + SftpPacket::Init(init_version_client) => { + return Err(SftpError::MalformedPacket); + } + SftpPacket::PathInfo(req_id, path_info) => { + let a_name = self + .file_server + .realpath(path_info.path.as_str()?)?; - match self.file_server.write( - &file_handle, - offset, - data_in_buffer.as_ref(), + let response = + SftpPacket::Name(req_id, a_name); + + response.encode_response(&mut sink)?; + } + SftpPacket::Open(req_id, open) => { + match self.file_server.open( + open.filename.as_str()?, + &open.attrs, ) { - Ok(_) => { - self.partial_write_request_tracker = - Some(write_tracker); + Ok(opaque_file_handle) => { + let response = SftpPacket::Handle( + req_id, + proto::Handle { + handle: opaque_file_handle + .into_file_handle(), + }, + ); + response + .encode_response(&mut sink)?; + info!("Sending '{:?}'", response); } + Err(status_code) => { + error!( + "Open failed: {:?}", + status_code + ); + push_general_failure( + req_id, "", &mut sink, + )?; + } + }; + } + SftpPacket::Write(req_id, write) => { + match self.file_server.write( + &T::try_from(&write.handle)?, + write.offset, + write.data.as_ref(), + ) { + Ok(_) => push_ok(req_id, &mut sink)?, Err(e) => { error!("SFTP write thrown: {:?}", e); push_general_failure( req_id, - "error writing ", + "error writing", &mut sink, - )?; + )? } }; } + SftpPacket::Close(req_id, close) => { + match self + .file_server + .close(&T::try_from(&close.handle)?) + { + Ok(_) => push_ok(req_id, &mut sink)?, + Err(e) => { + error!("SFTP Close thrown: {:?}", e); + push_general_failure( + req_id, "", &mut sink, + )? + } + } + } _ => { - push_general_failure( + error!("Unsuported request type"); + push_unsupported(ReqId(0), &mut sink)?; + } + }, + Err(e) => match e { + WireError::RanOut => { + warn!( + "RanOut for the SFTP Packet in the source buffer: {:?}", + e + ); + + match self.process_ran_out(&mut sink, &mut source) { + Ok(_) => { + self.state = + SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) + } + Err(e) => match e { + SftpError::WireError(WireError::RanOut) => { + let read = incomplete_request_holder + .try_hold( + &buffer_in + [buffer_in_remaining_index..], + )?; // Fails because it does not fit. Also. It is not the beginning of a new packet + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest) + } + _ => return (Err(SunsetError::Bug.into())), + }, + }; + } + WireError::UnknownPacket { number: _ } => { + warn!("Error decoding SFTP Packet:{:?}", e); + push_unsupported( ReqId(u32::MAX), - "Unsupported Request: Too long", &mut sink, )?; } - }; - } - WireError::UnknownPacket { number: _ } => { - warn!("Error decoding SFTP Packet:{:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; - } - _ => { - error!("Error decoding SFTP Packet: {:?}", e); - push_unsupported(ReqId(u32::MAX), &mut sink)?; - } + _ => { + error!( + "Error decoding SFTP Packet: {:?}", + e + ); + push_unsupported( + ReqId(u32::MAX), + &mut sink, + )?; + } + }, + }; } } + buffer_in_remaining_index = in_len - source.remaining(); } }; + used_out_accumulated_index += sink.finalize(); + } - // We will use these indexes to create new source and sink to process extra requests in the buffer. - buffer_in_remaining_index = in_len - source.remaining(); - if source.remaining() > 0 { - debug!( - "After processing request: Source bytes remaining = {:?}, buffer_in len = {:?} => buffer_in_remaining_index = {:?}", - source.remaining(), - in_len, - buffer_in_remaining_index - ); - trace!("Source dump: {:?}", source); - debug!( - "Buffer in left to process: {:?}", - &buffer_in[buffer_in_remaining_index..] + Ok(used_out_accumulated_index) + } + + fn process_ran_out( + &mut self, + sink: &mut SftpSink<'_>, + source: &mut SftpSource<'_>, + ) -> Result<(), SftpError> { + let packet_type = source.peak_packet_type()?; + match packet_type { + SftpNum::SSH_FXP_WRITE => { + debug!("about to decode packet partial write content",); + let ( + obscured_file_handle, + req_id, + offset, + data_in_buffer, + write_tracker, + ) = source.dec_packet_partial_write_content_and_get_tracker()?; + + trace!( + "obscured_file_handle = {:?}, req_id = {:?}, \ + offset = {:?}, data_in_buffer = {:?}, \ + write_tracker = {:?}", + obscured_file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle + req_id, + offset, + data_in_buffer, + write_tracker, ); + + match self.file_server.write( + &obscured_file_handle, + offset, + data_in_buffer.as_ref(), + ) { + Ok(_) => { + self.partial_write_request_tracker = Some(write_tracker); + } + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure(req_id, "error writing ", sink)?; + } + }; } + _ => { + error!("Packet type could not be handled {:?}", packet_type); + push_general_failure( + ReqId(u32::MAX), + "Unsupported Request: Too long", + sink, + )?; + } + }; + Ok(()) + } - // TODO: What about buffer_out overflow condition? - used_out_accumulated_index += sink.finalize(); + async fn process_known_request( + // &self, + // &mut self, + initialized: &mut bool, + file_server: &mut S, + sink: &mut SftpSink<'_>, + request: SftpPacket<'_>, + ) -> Result<(), WireError> { + if !*initialized && !matches!(request, SftpPacket::Init(_)) { + push_general_failure(ReqId(u32::MAX), "Not Initialized", sink)?; + error!("Request sent before init: {:?}", request); + return Ok(()); } + match request { + SftpPacket::Init(_) => { + // TODO: Do a real check, provide the lowest version or return an error if the + // client cannot handle the server SFTP_VERSION + let version = + SftpPacket::Version(InitVersionLowest { version: SFTP_VERSION }); - Ok(used_out_accumulated_index) + info!("Sending '{:?}'", version); + + version.encode_response(sink)?; + + *initialized = true; + } + SftpPacket::PathInfo(req_id, path_info) => { + let a_name = + file_server + .realpath(path_info.path.as_str().expect( + "Could not deref and the errors are not harmonized", + )) + .expect("Could not deref and the errors are not harmonized"); + + let response = SftpPacket::Name(req_id, a_name); + + response.encode_response(sink)?; + } + SftpPacket::Open(req_id, open) => { + match file_server.open(open.filename.as_str()?, &open.attrs) { + Ok(opaque_file_handle) => { + let response = SftpPacket::Handle( + req_id, + proto::Handle { + handle: opaque_file_handle.into_file_handle(), + }, + ); + response.encode_response(sink)?; + info!("Sending '{:?}'", response); + } + Err(status_code) => { + error!("Open failed: {:?}", status_code); + push_general_failure(req_id, "", sink)?; + } + }; + } + SftpPacket::Write(req_id, write) => { + match file_server.write( + &T::try_from(&write.handle)?, + write.offset, + write.data.as_ref(), + ) { + Ok(_) => push_ok(req_id, sink)?, + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure(req_id, "error writing ", sink)? + } + }; + } + SftpPacket::Close(req_id, close) => { + match file_server.close(&T::try_from(&close.handle)?) { + Ok(_) => push_ok(req_id, sink)?, + Err(e) => { + error!("SFTP Close thrown: {:?}", e); + push_general_failure(req_id, "", sink)? + } + } + } + _ => { + error!("This kind of request is not supported: {:?}", request); + push_unsupported(ReqId(0), sink)?; + } + }; + Ok(()) } - async fn process_known_request( - &mut self, + async fn process_request( + // &self, + // &mut self, + initialized: &mut bool, + file_server: &mut S, sink: &mut SftpSink<'_>, request: SftpPacket<'_>, ) -> Result<(), WireError> { - if !self.initialized && !matches!(request, SftpPacket::Init(_)) { + if !*initialized && !matches!(request, SftpPacket::Init(_)) { push_general_failure(ReqId(u32::MAX), "Not Initialized", sink)?; error!("Request sent before init: {:?}", request); return Ok(()); } match request { SftpPacket::Init(_) => { - // TODO: Do a real check, provide the lowest version or return an error if the client cannot handle the server SFTP_VERSION + // TODO: Do a real check, provide the lowest version or return an error if the + // client cannot handle the server SFTP_VERSION let version = SftpPacket::Version(InitVersionLowest { version: SFTP_VERSION }); @@ -274,11 +596,11 @@ where version.encode_response(sink)?; - self.initialized = true; + *initialized = true; } SftpPacket::PathInfo(req_id, path_info) => { let a_name = - self.file_server + file_server .realpath(path_info.path.as_str().expect( "Could not deref and the errors are not harmonized", )) @@ -289,7 +611,7 @@ where response.encode_response(sink)?; } SftpPacket::Open(req_id, open) => { - match self.file_server.open(open.filename.as_str()?, &open.attrs) { + match file_server.open(open.filename.as_str()?, &open.attrs) { Ok(opaque_file_handle) => { let response = SftpPacket::Handle( req_id, @@ -307,7 +629,7 @@ where }; } SftpPacket::Write(req_id, write) => { - match self.file_server.write( + match file_server.write( &T::try_from(&write.handle)?, write.offset, write.data.as_ref(), @@ -320,7 +642,7 @@ where }; } SftpPacket::Close(req_id, close) => { - match self.file_server.close(&T::try_from(&close.handle)?) { + match file_server.close(&T::try_from(&close.handle)?) { Ok(_) => push_ok(req_id, sink)?, Err(e) => { error!("SFTP Close thrown: {:?}", e); @@ -329,6 +651,7 @@ where } } _ => { + error!("This kind of request is not supported: {:?}", request); push_unsupported(ReqId(0), sink)?; } }; diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 1a6c1bb2..2760155d 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -1,6 +1,6 @@ use crate::proto::{ - ReqId, SFTP_FIELD_ID_INDEX, SFTP_MINIMUM_PACKET_LEN, SFTP_WRITE_REQID_INDEX, - SftpNum, + ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, + SFTP_MINIMUM_PACKET_LEN, SFTP_WRITE_REQID_INDEX, SftpNum, }; use crate::sftphandle::PartialWriteRequestTracker; use crate::{FileHandle, OpaqueFileHandle}; @@ -98,12 +98,10 @@ impl<'de> SftpSource<'de> { _ => return Err(WireError::PacketWrong), // TODO: Find a better error }; - let prev_index = self.index; self.index = SFTP_WRITE_REQID_INDEX; let req_id = ReqId::dec(self)?; let file_handle = FileHandle::dec(self)?; - let obscured_file_handle = OpaqueFileHandle::try_from(&file_handle)?; let offset = u64::dec(self)?; let data_len = u32::dec(self)?; @@ -124,6 +122,7 @@ impl<'de> SftpSource<'de> { remain_data_offset, )?; + let obscured_file_handle = OpaqueFileHandle::try_from(&file_handle)?; Ok((obscured_file_handle, req_id, offset, data_in_buffer, write_tracker)) } From ac0ed594a25ea7d281589c2337ee6aaad31675f1 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 2 Oct 2025 14:58:59 +1000 Subject: [PATCH 300/393] WIP: handling ok request in FragmentedRequestState::ProcessingClippedRequest This does not cover what happens with NoRoom requests such as Write request --- sftp/src/sftphandle.rs | 189 +++++++++++++++++++++++------------------ 1 file changed, 106 insertions(+), 83 deletions(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 4eb3ead9..95362486 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -143,15 +143,25 @@ where match e { RequestHolderError::RanOut => { warn!( - "There was not enough bytes in the buffer_in. We will continue adding bytes" + "There was not enough bytes in the buffer_in. \ + We will continue adding bytes" ); buffer_in_remaining_index += incomplete_request_holder.appended(); continue; } RequestHolderError::NoRoom => { - todo!( - "This is an incomplete long packet situation. You hope that it is a Write request" + warn!( + "There is not enough room in incomplete request holder \ + to accommodate this packet buffer." + ) + } + RequestHolderError::WireError( + WireError::RanOut, + ) => { + warn!( + "There is not enough room in incomplete request holder \ + to accommodate this packet buffer." ) } _ => { @@ -162,6 +172,10 @@ where return Err(SunsetError::Bug.into()); } } + } else { + debug!( + "Incomplete request holder completes request!" + ); } let mut source = SftpSource::new( &incomplete_request_holder.try_get_ref()?, @@ -171,13 +185,31 @@ where let sftp_packet = SftpPacket::decode_request(&mut source); - // TODO: Here we have to check the append_result or simply the sftp_packet to handle Errors such as TooLongRanOut or similar + match sftp_packet { + Ok(request) => { + self.handle_general_request(&mut sink, request)?; + } + Err(e) => match e { + WireError::NoRoom => { + todo!("The packet do not fit in the buffer") + } + WireError::RanOut => { + todo!("Not enough data to decode the packet") + } + _ => { + error!( + "Unhandled error decoding assembled packet: {:?}", + e + ); + } + }, + } let used = incomplete_request_holder.appended(); buffer_in_remaining_index += used; incomplete_request_holder.reset(); - + self.state = SftpHandleState::Idle; todo!("FragmentedRequestState::ProcessingClippedRequest") } FragmentedRequestState::ProcessingLongRequest => { @@ -299,84 +331,9 @@ where }, SftpHandleState::Idle => { match sftp_packet { - Ok(request) => match request { - SftpPacket::Init(init_version_client) => { - return Err(SftpError::MalformedPacket); - } - SftpPacket::PathInfo(req_id, path_info) => { - let a_name = self - .file_server - .realpath(path_info.path.as_str()?)?; - - let response = - SftpPacket::Name(req_id, a_name); - - response.encode_response(&mut sink)?; - } - SftpPacket::Open(req_id, open) => { - match self.file_server.open( - open.filename.as_str()?, - &open.attrs, - ) { - Ok(opaque_file_handle) => { - let response = SftpPacket::Handle( - req_id, - proto::Handle { - handle: opaque_file_handle - .into_file_handle(), - }, - ); - response - .encode_response(&mut sink)?; - info!("Sending '{:?}'", response); - } - Err(status_code) => { - error!( - "Open failed: {:?}", - status_code - ); - push_general_failure( - req_id, "", &mut sink, - )?; - } - }; - } - SftpPacket::Write(req_id, write) => { - match self.file_server.write( - &T::try_from(&write.handle)?, - write.offset, - write.data.as_ref(), - ) { - Ok(_) => push_ok(req_id, &mut sink)?, - Err(e) => { - error!("SFTP write thrown: {:?}", e); - push_general_failure( - req_id, - "error writing", - &mut sink, - )? - } - }; - } - SftpPacket::Close(req_id, close) => { - match self - .file_server - .close(&T::try_from(&close.handle)?) - { - Ok(_) => push_ok(req_id, &mut sink)?, - Err(e) => { - error!("SFTP Close thrown: {:?}", e); - push_general_failure( - req_id, "", &mut sink, - )? - } - } - } - _ => { - error!("Unsuported request type"); - push_unsupported(ReqId(0), &mut sink)?; - } - }, + Ok(request) => { + self.handle_general_request(&mut sink, request)? + } Err(e) => match e { WireError::RanOut => { warn!( @@ -432,6 +389,72 @@ where Ok(used_out_accumulated_index) } + fn handle_general_request( + &mut self, + sink: &mut SftpSink<'_>, + request: SftpPacket<'_>, + ) -> Result<(), SftpError> + where + T: OpaqueFileHandle, + { + Ok(match request { + SftpPacket::Init(init_version_client) => { + return Err(SftpError::MalformedPacket); + } + SftpPacket::PathInfo(req_id, path_info) => { + let a_name = self.file_server.realpath(path_info.path.as_str()?)?; + + let response = SftpPacket::Name(req_id, a_name); + + response.encode_response(sink)?; + } + SftpPacket::Open(req_id, open) => { + match self.file_server.open(open.filename.as_str()?, &open.attrs) { + Ok(opaque_file_handle) => { + let response = SftpPacket::Handle( + req_id, + proto::Handle { + handle: opaque_file_handle.into_file_handle(), + }, + ); + response.encode_response(sink)?; + info!("Sending '{:?}'", response); + } + Err(status_code) => { + error!("Open failed: {:?}", status_code); + push_general_failure(req_id, "", sink)?; + } + }; + } + SftpPacket::Write(req_id, write) => { + match self.file_server.write( + &T::try_from(&write.handle)?, + write.offset, + write.data.as_ref(), + ) { + Ok(_) => push_ok(req_id, sink)?, + Err(e) => { + error!("SFTP write thrown: {:?}", e); + push_general_failure(req_id, "error writing", sink)? + } + }; + } + SftpPacket::Close(req_id, close) => { + match self.file_server.close(&T::try_from(&close.handle)?) { + Ok(_) => push_ok(req_id, sink)?, + Err(e) => { + error!("SFTP Close thrown: {:?}", e); + push_general_failure(req_id, "", sink)? + } + } + } + _ => { + error!("Unsuported request type"); + push_unsupported(ReqId(0), sink)?; + } + }) + } + fn process_ran_out( &mut self, sink: &mut SftpSink<'_>, From b14ffa9dda390fdc76b965b3386198907b1213f2 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 2 Oct 2025 15:23:06 +1000 Subject: [PATCH 301/393] WIP: Removing dead code --- sftp/src/sftphandle.rs | 171 ----------------------------------------- 1 file changed, 171 deletions(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 95362486..d2ba05f8 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -509,177 +509,6 @@ where Ok(()) } - async fn process_known_request( - // &self, - // &mut self, - initialized: &mut bool, - file_server: &mut S, - sink: &mut SftpSink<'_>, - request: SftpPacket<'_>, - ) -> Result<(), WireError> { - if !*initialized && !matches!(request, SftpPacket::Init(_)) { - push_general_failure(ReqId(u32::MAX), "Not Initialized", sink)?; - error!("Request sent before init: {:?}", request); - return Ok(()); - } - match request { - SftpPacket::Init(_) => { - // TODO: Do a real check, provide the lowest version or return an error if the - // client cannot handle the server SFTP_VERSION - let version = - SftpPacket::Version(InitVersionLowest { version: SFTP_VERSION }); - - info!("Sending '{:?}'", version); - - version.encode_response(sink)?; - - *initialized = true; - } - SftpPacket::PathInfo(req_id, path_info) => { - let a_name = - file_server - .realpath(path_info.path.as_str().expect( - "Could not deref and the errors are not harmonized", - )) - .expect("Could not deref and the errors are not harmonized"); - - let response = SftpPacket::Name(req_id, a_name); - - response.encode_response(sink)?; - } - SftpPacket::Open(req_id, open) => { - match file_server.open(open.filename.as_str()?, &open.attrs) { - Ok(opaque_file_handle) => { - let response = SftpPacket::Handle( - req_id, - proto::Handle { - handle: opaque_file_handle.into_file_handle(), - }, - ); - response.encode_response(sink)?; - info!("Sending '{:?}'", response); - } - Err(status_code) => { - error!("Open failed: {:?}", status_code); - push_general_failure(req_id, "", sink)?; - } - }; - } - SftpPacket::Write(req_id, write) => { - match file_server.write( - &T::try_from(&write.handle)?, - write.offset, - write.data.as_ref(), - ) { - Ok(_) => push_ok(req_id, sink)?, - Err(e) => { - error!("SFTP write thrown: {:?}", e); - push_general_failure(req_id, "error writing ", sink)? - } - }; - } - SftpPacket::Close(req_id, close) => { - match file_server.close(&T::try_from(&close.handle)?) { - Ok(_) => push_ok(req_id, sink)?, - Err(e) => { - error!("SFTP Close thrown: {:?}", e); - push_general_failure(req_id, "", sink)? - } - } - } - _ => { - error!("This kind of request is not supported: {:?}", request); - push_unsupported(ReqId(0), sink)?; - } - }; - Ok(()) - } - - async fn process_request( - // &self, - // &mut self, - initialized: &mut bool, - file_server: &mut S, - sink: &mut SftpSink<'_>, - request: SftpPacket<'_>, - ) -> Result<(), WireError> { - if !*initialized && !matches!(request, SftpPacket::Init(_)) { - push_general_failure(ReqId(u32::MAX), "Not Initialized", sink)?; - error!("Request sent before init: {:?}", request); - return Ok(()); - } - match request { - SftpPacket::Init(_) => { - // TODO: Do a real check, provide the lowest version or return an error if the - // client cannot handle the server SFTP_VERSION - let version = - SftpPacket::Version(InitVersionLowest { version: SFTP_VERSION }); - - info!("Sending '{:?}'", version); - - version.encode_response(sink)?; - - *initialized = true; - } - SftpPacket::PathInfo(req_id, path_info) => { - let a_name = - file_server - .realpath(path_info.path.as_str().expect( - "Could not deref and the errors are not harmonized", - )) - .expect("Could not deref and the errors are not harmonized"); - - let response = SftpPacket::Name(req_id, a_name); - - response.encode_response(sink)?; - } - SftpPacket::Open(req_id, open) => { - match file_server.open(open.filename.as_str()?, &open.attrs) { - Ok(opaque_file_handle) => { - let response = SftpPacket::Handle( - req_id, - proto::Handle { - handle: opaque_file_handle.into_file_handle(), - }, - ); - response.encode_response(sink)?; - info!("Sending '{:?}'", response); - } - Err(status_code) => { - error!("Open failed: {:?}", status_code); - push_general_failure(req_id, "", sink)?; - } - }; - } - SftpPacket::Write(req_id, write) => { - match file_server.write( - &T::try_from(&write.handle)?, - write.offset, - write.data.as_ref(), - ) { - Ok(_) => push_ok(req_id, sink)?, - Err(e) => { - error!("SFTP write thrown: {:?}", e); - push_general_failure(req_id, "error writing ", sink)? - } - }; - } - SftpPacket::Close(req_id, close) => { - match file_server.close(&T::try_from(&close.handle)?) { - Ok(_) => push_ok(req_id, sink)?, - Err(e) => { - error!("SFTP Close thrown: {:?}", e); - push_general_failure(req_id, "", sink)? - } - } - } - _ => { - error!("This kind of request is not supported: {:?}", request); - push_unsupported(ReqId(0), sink)?; - } - }; - Ok(()) - } } #[inline] From 67735c335a1540f8ecf98c3aa5db7ec04e2226db Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 2 Oct 2025 15:32:05 +1000 Subject: [PATCH 302/393] WIP: replacing WireErrors and minor renaming --- sftp/src/requestholder.rs | 252 +++++++++++++++++++++++++------------- sftp/src/sftperror.rs | 26 +++- sftp/src/sftphandle.rs | 93 +++++++++----- sftp/src/sftpsource.rs | 11 +- 4 files changed, 256 insertions(+), 126 deletions(-) diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs index 7b6df9b7..36f37fa3 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/requestholder.rs @@ -18,6 +18,7 @@ pub(crate) enum RequestHolderError { RanOut, /// WireError WireError(WireError), + Bug, } impl From for RequestHolderError { @@ -57,7 +58,7 @@ pub struct RequestHolder<'a> { buffer: &'a mut [u8], /// The index of the last byte in the buffer containing usable data buffer_fill_index: usize, - /// Number of bytes appended to foundational `try_hold` slice + /// Number of bytes appended in a previous `try_hold` or `try_append_for_valid_request` slice appended: usize, /// Marks the structure as invalid invalid: bool, @@ -72,42 +73,26 @@ impl<'a> RequestHolder<'a> { RequestHolder { buffer: buffer, buffer_fill_index: 0, - invalid: false, + invalid: false, // TODO: Remove if invalidate() is removed busy: false, appended: 0, } } - /// Resets the structure allowing it to hold a new request. - /// - /// Will not remove the previous data from the buffer - pub fn reset(&mut self) -> () { - self.busy = false; - self.invalid = false; - self.buffer_fill_index = 0; - self.appended = 0; - } - - /// Invalidates the current instance and returns its original buffer. Does not erase previous data - pub fn invalidate(&mut self) -> RequestHolderResult<&[u8]> { - if !self.busy { - return Err(RequestHolderError::Empty); - } - if self.invalid { - return Err(RequestHolderError::Invalid); - } - - self.invalid = true; - // self.buffer_fill_index = 0; - Ok(&self.buffer) - } - /// Uses the internal buffer to store a copy of the provided slice /// /// The definition of `try_hold` and `try_append_slice` separately /// is deliberated to follow an order in composing the held request /// - /// returns the number of bytes read from the slice + /// Increases the `appended()` counter + /// + /// returns: + /// + /// - Ok(usize): the number of bytes read from the slice + /// + /// - `Err(Busy)`: If there has been a call to `try_hold` without a call to `reset` + /// + /// - `Err(Invalid)`: If the structure has been marked as invalid previously pub fn try_hold(&mut self, slice: &[u8]) -> RequestHolderResult { if self.busy { return Err(RequestHolderError::Busy); @@ -123,55 +108,56 @@ impl<'a> RequestHolder<'a> { Ok(read_in) } - /// Appends a slice to the internal buffer. Requires the buffer to - /// be busy by using `try_hold` first + /// Resets the structure allowing it to hold a new request. /// - /// Increases the `appended` counter + /// Resets the `appended()` counter. /// - /// Returns the number of bytes appended - fn try_append_slice(&mut self, slice: &[u8]) -> RequestHolderResult<()> { - if slice.len() == 0 { - warn!("try appending a zero length slice"); - return Ok(()); - } + /// Will not clear the previous data from the buffer. + pub fn reset(&mut self) -> () { + self.busy = false; + self.invalid = false; + self.buffer_fill_index = 0; + self.appended = 0; + } + + // TODO: Remove it if it is not used + /// Invalidates the current instance and returns its original buffer. Does not erase previous data + pub fn invalidate(&mut self) -> RequestHolderResult<&[u8]> { if !self.busy { return Err(RequestHolderError::Empty); } - if self.invalid { return Err(RequestHolderError::Invalid); } - let in_len = slice.len(); - if in_len > self.remaining_len() { - return Err(RequestHolderError::NoRoom); - } - debug!("Adding: {:?}", slice); - - self.buffer[self.buffer_fill_index..self.buffer_fill_index + in_len] - .copy_from_slice(slice); - - self.buffer_fill_index += in_len; - debug!( - "RequestHolder: index = {:?}, slice = {:?}", - self.buffer_fill_index, - self.try_get_ref()? - ); - self.appended += in_len; - Ok(()) + self.invalid = true; + // self.buffer_fill_index = 0; + Ok(&self.buffer) } /// Using the content of the `RequestHolder` tries to find a valid /// SFTP request appending from slice into the internal buffer to - /// form a valid request + /// form a valid request. + /// + /// Reset and increase the `appended()` counter. + /// + /// **Returns**: + /// + /// - `Ok(())`: Full valid request + /// + /// - `Err(RanOut)`: Not enough bytes in the slice to complete a valid request or fill the buffer + /// + /// - `Err(NoRoom)`: The internal buffer is full but there is not a full valid request in the buffer + /// + /// - `Err(Invalid)`: If the structure has been marked as invalid previously + /// + /// - `Err(Empty)`: If the structure has not been loaded with `try_hold` /// - /// Returns the number of bytes appended + /// - `Err(Bug)`: An unexpected condition arises pub fn try_append_for_valid_request( &mut self, slice: &[u8], ) -> RequestHolderResult<()> { - self.appended = 0; // reset appended bytes counter - debug!( "try_append_for_valid_request: self = {:?}\n\ Space left = {:?}\n\ @@ -182,21 +168,37 @@ impl<'a> RequestHolder<'a> { ); if self.invalid { + error!("Request Holder is invalid"); return Err(RequestHolderError::Invalid); } if !self.busy { + error!("Request Holder is not busy"); return Err(RequestHolderError::Empty); } - // This makes sure that we do not try to read more slice than we can + if self.is_full() { + error!("Request Holder is full"); + return Err(RequestHolderError::NoRoom); + } + + self.appended = 0; // reset appended bytes counter + + // If we will not be able to read the SFTP packet ID we clearly need more data if self.buffer_fill_index + slice.len() < proto::SFTP_FIELD_ID_INDEX { self.try_append_slice(&slice)?; + error!( + "[Buffer fill index = {:?}] + [slice.len = {:?}] = {:?} < SFTP field id index = {:?}", + self.buffer_fill_index, + slice.len(), + self.buffer_fill_index + slice.len(), + proto::SFTP_FIELD_ID_INDEX + ); return Err(RequestHolderError::RanOut); } let complete_to_id_index = proto::SFTP_FIELD_ID_INDEX - .checked_sub(self.buffer_fill_index - 1) + .checked_sub(self.buffer_fill_index) .unwrap_or(0); if complete_to_id_index > 0 { @@ -220,39 +222,66 @@ impl<'a> RequestHolder<'a> { }; } - let packet_len = - SftpSource::new(self.try_get_ref()?).peak_packet_len()? as usize; - - let packet_type = SftpSource::new(self.try_get_ref()?).peak_packet_type()?; + let (packet_len, packet_type) = { + let temp_source = SftpSource::new(self.try_get_ref()?); + let packet_len = temp_source.peak_packet_len()? as usize; + let packet_type = temp_source.peak_packet_type()?; + (packet_len, packet_type) + }; debug!("Request len = {:?}, type = {:?}", packet_len, packet_type); - let remaining_packet_len = packet_len - self.buffer_fill_index; // TODO: Careful, the packet len does not include the packet len field + let remaining_packet_len = + packet_len - (self.buffer_fill_index - proto::SFTP_FIELD_LEN_LENGTH); + // The packet len does not include the packet len field itself (4 bytes) + // https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-3 + debug!( + "[Total Packet len = {:?}] = [Packet len copied so far = {:}] \ + - [SFTP Field len length = {:?}] + [Remaining packet len = {:?}]", + packet_len, + self.buffer_fill_index, + proto::SFTP_FIELD_LEN_LENGTH, + remaining_packet_len, + ); + assert_eq!( + packet_len, + self.buffer_fill_index - proto::SFTP_FIELD_LEN_LENGTH + + remaining_packet_len + ); + // TODO: Fix the mess with the logic and the indexes to address the slice. IT IS PANICKING + if remaining_packet_len <= self.remaining_len() { + // We have all the remaining packet bytes in the slice and fits in the buffer - if remaining_packet_len <= (self.remaining_len()) { - if remaining_packet_len > slice.len() { + if (slice.len()) < (remaining_packet_len + self.appended()) { self.try_append_slice(&slice[self.appended()..])?; - error!( - "The slice to include to the held fragment does not \ - contain the whole packet. More data is required." - ); return Err(RequestHolderError::RanOut); + } else { + self.try_append_slice( + &slice[self.appended()..remaining_packet_len], + )?; + return Ok(()); } - self.try_append_slice(&slice[self.appended()..])?; // The only Ok } else { - warn!( - "The request does not fit in the buffer: \ - (req len = {:?} > buffer len = {:?} )", - packet_len, - self.buffer.len() + // the remaining packet bytes are more than we can fit in the buffer + // But they may not fit in the slice neither + + let start = self.appended(); + let end = self.remaining_len().min(slice.len() - self.appended()); + + debug!( + "Will finally take the range: [{:?}..{:?}] from the slice [0..{:?}]", + start, + end, + slice.len() ); - if self.remaining_len() < (slice.len() - self.appended()) { - self.try_append_slice( - &slice[self.appended()..self.remaining_len()], - )?; + self.try_append_slice( + &slice[self.appended() + ..self.remaining_len().min(slice.len() - self.appended())], + )?; + if self.is_full() { + return Err(RequestHolderError::NoRoom); } else { - self.try_append_slice(&slice[self.appended()..])?; + return Err(RequestHolderError::RanOut); } - return Err(RequestHolderError::NoRoom); } Ok(()) } @@ -264,12 +293,20 @@ impl<'a> RequestHolder<'a> { } if self.busy { + debug!( + "Returning reference to: {:?}", + &self.buffer[..self.buffer_fill_index] + ); Ok(&self.buffer[..self.buffer_fill_index]) } else { Err(RequestHolderError::Empty) } } + pub fn is_full(&mut self) -> bool { + self.buffer_fill_index == self.buffer.len() + } + /// Returns true if it has a slice in its buffer pub fn is_busy(&self) -> bool { self.busy @@ -280,9 +317,56 @@ impl<'a> RequestHolder<'a> { self.appended } + /// Appends a slice to the internal buffer. Requires the buffer to + /// be busy by using `try_hold` first + /// + /// Increases the `appended` counter but does not reset it + /// + /// Returns: + /// + /// - `Ok(())`: the slice was appended + /// + /// - `Err(Invalid)`: If the structure has been marked as invalid previously + /// + /// - `Err(Empty)`: If the structure has not been loaded with `try_hold` + /// + /// - `Err(NoRoom)`: The internal buffer is full but there is not a full valid request in the buffer + fn try_append_slice(&mut self, slice: &[u8]) -> RequestHolderResult<()> { + if slice.len() == 0 { + warn!("try appending a zero length slice"); + return Ok(()); + } + if !self.busy { + return Err(RequestHolderError::Empty); + } + + if self.invalid { + return Err(RequestHolderError::Invalid); + } + + let in_len = slice.len(); + if in_len > self.remaining_len() { + return Err(RequestHolderError::NoRoom); + } + debug!("Adding: {:?}", slice); + + self.buffer[self.buffer_fill_index..self.buffer_fill_index + in_len] + .copy_from_slice(slice); + + self.buffer_fill_index += in_len; + debug!( + "RequestHolder: index = {:?}, slice = {:?}", + self.buffer_fill_index, + self.try_get_ref()? + ); + self.appended += in_len; + Ok(()) + } + /// Returns the number of bytes unused at the end of the buffer, /// this is, the remaining length fn remaining_len(&self) -> usize { - self.buffer.len() - self.buffer_fill_index - 1 // Off by one? + // self.buffer.len() - self.buffer_fill_index - 1 // TODO: Off by one? + self.buffer.len() - self.buffer_fill_index } } diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index 98192a4d..a97d52fd 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -1,5 +1,6 @@ use core::convert::From; +use log::warn; use sunset::Error as SunsetError; use sunset::sshwire::WireError; @@ -10,6 +11,7 @@ pub enum SftpError { NotInitialized, AlreadyInitialized, MalformedPacket, + NotSupported, WireError(WireError), OperationError(StatusCode), SunsetError(SunsetError), @@ -54,10 +56,26 @@ impl From for SunsetError { match value { SftpError::SunsetError(error) => error, SftpError::WireError(wire_error) => wire_error.into(), - SftpError::NotInitialized => SunsetError::PacketWrong {}, - SftpError::AlreadyInitialized => SunsetError::PacketWrong {}, - SftpError::MalformedPacket => SunsetError::PacketWrong {}, - SftpError::OperationError(_) => SunsetError::PacketWrong {}, + SftpError::NotInitialized => { + warn!("Casting error loosing information: {:?}", value); + SunsetError::PacketWrong {} + } + SftpError::NotSupported => { + warn!("Casting error loosing information: {:?}", value); + SunsetError::PacketWrong {} + } + SftpError::AlreadyInitialized => { + warn!("Casting error loosing information: {:?}", value); + SunsetError::PacketWrong {} + } + SftpError::MalformedPacket => { + warn!("Casting error loosing information: {:?}", value); + SunsetError::PacketWrong {} + } + SftpError::OperationError(_) => { + warn!("Casting error loosing information: {:?}", value); + SunsetError::PacketWrong {} + } SftpError::RequestHolderError(request_holder_error) => SunsetError::Bug, } } diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index d2ba05f8..0dd63d06 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -115,9 +115,6 @@ where trace!("Received {:} bytes to process", in_len); - // let mut pending_incomplete_request = incomplete_request_holder.is_busy(); - // let pending_long_request = self.partial_write_request_tracker.is_some(); - if !matches!(self.state, SftpHandleState::Fragmented(_)) & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) { @@ -134,12 +131,13 @@ where SftpHandleState::Fragmented(fragment_case) => { match fragment_case { FragmentedRequestState::ProcessingClippedRequest => { - let append_result = incomplete_request_holder + let appending_result = incomplete_request_holder .try_append_for_valid_request( + // TODO: All your problems are here. Focus &buffer_in[buffer_in_remaining_index..], ); - if let Err(e) = append_result { + if let Err(e) = appending_result { match e { RequestHolderError::RanOut => { warn!( @@ -151,6 +149,7 @@ where continue; } RequestHolderError::NoRoom => { + // Fragmented Write request... warn!( "There is not enough room in incomplete request holder \ to accommodate this packet buffer." @@ -160,7 +159,7 @@ where WireError::RanOut, ) => { warn!( - "There is not enough room in incomplete request holder \ + "WireError: There is not enough room in incomplete request holder \ to accommodate this packet buffer." ) } @@ -174,43 +173,62 @@ where } } else { debug!( - "Incomplete request holder completes request!" + "Incomplete request holder completed the request!" ); } + + let used = incomplete_request_holder.appended(); + buffer_in_remaining_index += used; + let mut source = SftpSource::new( &incomplete_request_holder.try_get_ref()?, ); trace!("Internal Source Content: {:?}", source); - let sftp_packet = + let decoding_request_result = SftpPacket::decode_request(&mut source); - match sftp_packet { + match decoding_request_result { Ok(request) => { self.handle_general_request(&mut sink, request)?; + incomplete_request_holder.reset(); + self.state = SftpHandleState::Idle; } Err(e) => match e { - WireError::NoRoom => { - todo!("The packet do not fit in the buffer") - } WireError::RanOut => { - todo!("Not enough data to decode the packet") + match self + .handle_ran_out(&mut sink, &mut source) + { + Ok(_) => { + self.state = + SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); + incomplete_request_holder.reset(); + } + Err(e) => match e { + _ => { + error!( + "handle_ran_out finished with error: {:?}", + e + ); + return (Err( + SunsetError::Bug.into() + )); + } + }, + } + } + WireError::NoRoom => { + error!("Not enough space to fit the request") } _ => { error!( "Unhandled error decoding assembled packet: {:?}", e ); + return Err(WireError::PacketWrong.into()); } }, } - - let used = incomplete_request_holder.appended(); - buffer_in_remaining_index += used; - - incomplete_request_holder.reset(); - self.state = SftpHandleState::Idle; - todo!("FragmentedRequestState::ProcessingClippedRequest") } FragmentedRequestState::ProcessingLongRequest => { let mut source = SftpSource::new( @@ -223,16 +241,19 @@ where { wt } else { - return Err(SftpError::SunsetError( - SunsetError::Bug, - )); + error!( + "BUG: FragmentedRequestState::ProcessingLongRequest cannot take the write tracker" + ); + return Err(SunsetError::Bug.into()); }; let opaque_handle = write_tracker.get_opaque_file_handle(); - let usable_data = - in_len.min(write_tracker.remain_data_len as usize); + let usable_data = source + .remaining() + .min(write_tracker.remain_data_len as usize); // TODO: Where does in_len comes from? + // in_len.min(write_tracker.remain_data_len as usize); let data_segment = // Fails!! source.dec_as_binstring(usable_data)?; @@ -341,7 +362,7 @@ where e ); - match self.process_ran_out(&mut sink, &mut source) { + match self.handle_ran_out(&mut sink, &mut source) { Ok(_) => { self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) @@ -357,7 +378,7 @@ where } _ => return (Err(SunsetError::Bug.into())), }, - }; + }; } WireError::UnknownPacket { number: _ } => { warn!("Error decoding SFTP Packet:{:?}", e); @@ -455,11 +476,15 @@ where }) } - fn process_ran_out( + /// Handles long request that do not fit in the buffers and stores a tracker + /// + /// **WARNING:** Only `SSH_FXP_WRITE` has been implemented! + /// + fn handle_ran_out( &mut self, sink: &mut SftpSink<'_>, source: &mut SftpSource<'_>, - ) -> Result<(), SftpError> { + ) -> SftpResult<()> { let packet_type = source.peak_packet_type()?; match packet_type { SftpNum::SSH_FXP_WRITE => { @@ -476,7 +501,7 @@ where "obscured_file_handle = {:?}, req_id = {:?}, \ offset = {:?}, data_in_buffer = {:?}, \ write_tracker = {:?}", - obscured_file_handle, // This file_handle will be the one facilitated by the demosftpserver, this is, an obscured file handle + obscured_file_handle, req_id, offset, data_in_buffer, @@ -489,7 +514,7 @@ where data_in_buffer.as_ref(), ) { Ok(_) => { - self.partial_write_request_tracker = Some(write_tracker); + self.partial_write_request_tracker = Some(write_tracker); // TODO: This might belong to return value } Err(e) => { error!("SFTP write thrown: {:?}", e); @@ -498,7 +523,10 @@ where }; } _ => { - error!("Packet type could not be handled {:?}", packet_type); + error!( + "RanOut of Packet type could not be handled {:?}", + packet_type + ); push_general_failure( ReqId(u32::MAX), "Unsupported Request: Too long", @@ -508,7 +536,6 @@ where }; Ok(()) } - } #[inline] diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 2760155d..7f5cf4a4 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -3,8 +3,9 @@ use crate::proto::{ SFTP_MINIMUM_PACKET_LEN, SFTP_WRITE_REQID_INDEX, SftpNum, }; use crate::sftphandle::PartialWriteRequestTracker; -use crate::{FileHandle, OpaqueFileHandle}; +use crate::{FileHandle, OpaqueFileHandle, SftpError, SftpResult}; +use sunset::error::RanOut; use sunset::sshwire::{BinString, SSHDecode, SSHSource, WireError, WireResult}; #[allow(unused_imports)] @@ -53,7 +54,7 @@ impl<'de> SftpSource<'de> { /// /// **Warning**: will only work in well formed packets, in other case the result will contain garbage pub(crate) fn peak_packet_type(&self) -> WireResult { - if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { + if self.buffer.len() < SFTP_FIELD_ID_INDEX { Err(WireError::RanOut) } else { Ok(SftpNum::from(self.buffer[SFTP_FIELD_ID_INDEX])) @@ -87,15 +88,15 @@ impl<'de> SftpSource<'de> { T: OpaqueFileHandle, >( &mut self, - ) -> WireResult<(T, ReqId, u64, BinString<'de>, PartialWriteRequestTracker)> + ) -> SftpResult<(T, ReqId, u64, BinString<'de>, PartialWriteRequestTracker)> { if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - return Err(WireError::PacketWrong); // TODO: Find a better error + return Err(WireError::RanOut.into()); } match self.peak_packet_type()? { SftpNum::SSH_FXP_WRITE => {} - _ => return Err(WireError::PacketWrong), // TODO: Find a better error + _ => return Err(SftpError::NotSupported), }; self.index = SFTP_WRITE_REQID_INDEX; From 69e5e22198a02429cffe8cd9672cd74f525e33ab Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 10:50:53 +1000 Subject: [PATCH 303/393] WIP: Fixing bugs RequestHolder Transition logic --- sftp/src/sftphandle.rs | 62 ++++++++++++++++++++++++------------------ sftp/src/sftpsource.rs | 2 +- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 0dd63d06..17f19729 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -125,19 +125,21 @@ where let mut sink = SftpSink::new(&mut buffer_out[used_out_accumulated_index..]); - debug!("SFTP Process State: {:?}", self.state); + debug!( + "<=======================[ SFTP Process State: {:?} ]=======================>", + self.state + ); match &self.state { SftpHandleState::Fragmented(fragment_case) => { match fragment_case { FragmentedRequestState::ProcessingClippedRequest => { - let appending_result = incomplete_request_holder + if let Err(e) = incomplete_request_holder .try_append_for_valid_request( // TODO: All your problems are here. Focus &buffer_in[buffer_in_remaining_index..], - ); - - if let Err(e) = appending_result { + ) + { match e { RequestHolderError::RanOut => { warn!( @@ -148,21 +150,23 @@ where incomplete_request_holder.appended(); continue; } - RequestHolderError::NoRoom => { - // Fragmented Write request... - warn!( - "There is not enough room in incomplete request holder \ - to accommodate this packet buffer." - ) - } RequestHolderError::WireError( WireError::RanOut, ) => { warn!( - "WireError: There is not enough room in incomplete request holder \ - to accommodate this packet buffer." + "WIRE ERROR: There was not enough bytes in the buffer_in. \ + We will continue adding bytes" + ); + buffer_in_remaining_index += + incomplete_request_holder.appended(); + continue; + } + RequestHolderError::NoRoom => { + warn!( + "The request holder if full but the request in incomplete" ) } + _ => { error!( "Unhandled error completing incomplete request {:?}", @@ -185,10 +189,7 @@ where ); trace!("Internal Source Content: {:?}", source); - let decoding_request_result = - SftpPacket::decode_request(&mut source); - - match decoding_request_result { + match SftpPacket::decode_request(&mut source) { Ok(request) => { self.handle_general_request(&mut sink, request)?; incomplete_request_holder.reset(); @@ -200,9 +201,8 @@ where .handle_ran_out(&mut sink, &mut source) { Ok(_) => { - self.state = - SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); incomplete_request_holder.reset(); + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); } Err(e) => match e { _ => { @@ -210,9 +210,9 @@ where "handle_ran_out finished with error: {:?}", e ); - return (Err( + return Err( SunsetError::Bug.into() - )); + ); } }, } @@ -367,17 +367,22 @@ where self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) } - Err(e) => match e { + Err(e) => { + error!("Error handle_ran_out"); + match e { SftpError::WireError(WireError::RanOut) => { + let read = incomplete_request_holder .try_hold( &buffer_in [buffer_in_remaining_index..], - )?; // Fails because it does not fit. Also. It is not the beginning of a new packet - self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest) + )?; + buffer_in_remaining_index += read; + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); + continue; } _ => return (Err(SunsetError::Bug.into())), - }, + }}, }; } WireError::UnknownPacket { number: _ } => { @@ -488,7 +493,7 @@ where let packet_type = source.peak_packet_type()?; match packet_type { SftpNum::SSH_FXP_WRITE => { - debug!("about to decode packet partial write content",); + debug!("about to decode packet partial write content. Source remaining = {:?}",source.remaining()); let ( obscured_file_handle, req_id, @@ -514,6 +519,9 @@ where data_in_buffer.as_ref(), ) { Ok(_) => { + debug!( + "Storing a write tracker for a fragmented write request" + ); self.partial_write_request_tracker = Some(write_tracker); // TODO: This might belong to return value } Err(e) => { diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 7f5cf4a4..f7f023ad 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -54,7 +54,7 @@ impl<'de> SftpSource<'de> { /// /// **Warning**: will only work in well formed packets, in other case the result will contain garbage pub(crate) fn peak_packet_type(&self) -> WireResult { - if self.buffer.len() < SFTP_FIELD_ID_INDEX { + if self.buffer.len() < SFTP_FIELD_ID_INDEX + 1 { Err(WireError::RanOut) } else { Ok(SftpNum::from(self.buffer[SFTP_FIELD_ID_INDEX])) From 855728bdd7abc097db1b38214eb051f3f8efac26 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 11:39:29 +1000 Subject: [PATCH 304/393] WIP: Successfully received 100MB and 1024MB It is time to tidy up --- demo/sftp/std/testing/test_limit_write_req.sh | 33 ---------------- .../std/testing/test_long_write_requests.sh | 38 +++++++++++++++++++ demo/sftp/std/testing/test_write_requests.sh | 7 ++-- sftp/src/requestholder.rs | 4 +- 4 files changed, 44 insertions(+), 38 deletions(-) delete mode 100755 demo/sftp/std/testing/test_limit_write_req.sh create mode 100755 demo/sftp/std/testing/test_long_write_requests.sh diff --git a/demo/sftp/std/testing/test_limit_write_req.sh b/demo/sftp/std/testing/test_limit_write_req.sh deleted file mode 100755 index 45614183..00000000 --- a/demo/sftp/std/testing/test_limit_write_req.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -# Set remote server details -REMOTE_HOST="192.168.69.2" -REMOTE_USER="any" - -# Generate random data files -echo "Generating random data files..." - -dd if=/dev/random bs=1024 count=65 of=./TwoWriteRequests_random 2>/dev/null # Fails - - -echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." -echo "Test Results:" -echo "=============" - -sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF -put ./TwoWriteRequests_random -bye -EOF - -diff ./TwoWriteRequests_random ./out/TwoWriteRequests_random -if [ $? -eq 0 ]; then - echo "PASS" -else - echo "FAIL" -fi - -echo "Cleaning up local files..." -rm -f ./*_random -rm -f ./out/*_random - -echo "Upload test completed." \ No newline at end of file diff --git a/demo/sftp/std/testing/test_long_write_requests.sh b/demo/sftp/std/testing/test_long_write_requests.sh new file mode 100755 index 00000000..a0104e17 --- /dev/null +++ b/demo/sftp/std/testing/test_long_write_requests.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Define test files +FILES=("100MB_random" "1024MB_random") + +# Generate random data files +echo "Generating random data files..." +dd if=/dev/random bs=1048576 count=100 of=./100MB_random 2>/dev/null +dd if=/dev/random bs=1048576 count=1024 of=./1024MB_random 2>/dev/null + +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." + +# Upload all files +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +$(printf 'put ./%s\n' "${FILES[@]}") +bye +EOF + +echo "Test Results:" +echo "=============" + +# Test each file +for file in "${FILES[@]}"; do + if diff "./${file}" "./out/${file}" >/dev/null 2>&1; then + echo "PASS: ${file}" + else + echo "FAIL: ${file}" + fi +done + +echo "Cleaning up local files..." +rm -f ./*_random ./out/*_random + +echo "Upload test completed." \ No newline at end of file diff --git a/demo/sftp/std/testing/test_write_requests.sh b/demo/sftp/std/testing/test_write_requests.sh index 8ac5d5e7..13a4dbcc 100755 --- a/demo/sftp/std/testing/test_write_requests.sh +++ b/demo/sftp/std/testing/test_write_requests.sh @@ -5,7 +5,7 @@ REMOTE_HOST="192.168.69.2" REMOTE_USER="any" # Define test files -FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random" "256kB_random" "1MB_random" "2MB_random") +FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random" "256kB_random" "1024kB_random" "2048kB_random") # Generate random data files echo "Generating random data files..." @@ -14,8 +14,9 @@ dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null dd if=/dev/random bs=1024 count=65 of=./65kB_random 2>/dev/null dd if=/dev/random bs=1024 count=256 of=./256kB_random 2>/dev/null -dd if=/dev/random bs=1048576 count=1 of=./1MB_random 2>/dev/null -dd if=/dev/random bs=1048576 count=2 of=./2MB_random 2>/dev/null +dd if=/dev/random bs=1024 count=1024 of=./1024kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=2048 of=./2048kB_random 2>/dev/null + echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs index 36f37fa3..af35e396 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/requestholder.rs @@ -197,13 +197,13 @@ impl<'a> RequestHolder<'a> { return Err(RequestHolderError::RanOut); } - let complete_to_id_index = proto::SFTP_FIELD_ID_INDEX + let complete_to_id_index = (proto::SFTP_FIELD_ID_INDEX + 1) .checked_sub(self.buffer_fill_index) .unwrap_or(0); if complete_to_id_index > 0 { warn!( - "The held fragment len = {:?}, is insufficient to peak\ + "The held fragment len = {:?}, is insufficient to peak \ the length and type. Will append {:?} to reach the \ id field index: {:?}", self.buffer_fill_index, From 7fe51a594f541b9a98bac01af10857e5de93f820 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 11:50:25 +1000 Subject: [PATCH 305/393] WIP: Added SftpError::FileServerError(StatusCode) + others - Reasonable warning removal - Adding comments - Removing dead code - More refactoring --- demo/sftp/std/src/main.rs | 14 +-- sftp/src/lib.rs | 2 - sftp/src/requestholder.rs | 5 +- sftp/src/sftperror.rs | 36 +++++-- sftp/src/sftphandle.rs | 213 +++++++++++++++++++++++++------------- sftp/src/sftpsource.rs | 5 - 6 files changed, 174 insertions(+), 101 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index b70e9046..74403ad6 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -1,6 +1,6 @@ use sunset::*; use sunset_async::{ProgressHolder, SSHServer, SunsetMutex, SunsetRawMutex}; -use sunset_sftp::{RequestHolder, SftpHandler}; +use sunset_sftp::SftpHandler; pub(crate) use sunset_demo_common as demo_common; @@ -141,6 +141,7 @@ impl DemoServer for StdDemo { Ok(()) }; + #[allow(unreachable_code)] let sftp_loop = async { loop { let ch = chan_pipe.receive().await; @@ -152,9 +153,6 @@ impl DemoServer for StdDemo { let mut buffer_out = [0u8; 384]; let mut incomplete_request_buffer = [0u8; 128]; // TODO: Find a non arbitrary length - let mut incomplete_request_holder = - RequestHolder::new(&mut incomplete_request_buffer); - match { let mut stdio = serv.stdio(ch).await?; let mut file_server = DemoSftpServer::new( @@ -164,7 +162,7 @@ impl DemoServer for StdDemo { let mut sftp_handler = SftpHandler::::new( &mut file_server, - // &mut incomplete_request_buffer, + &mut incomplete_request_buffer, ); loop { let lr = stdio.read(&mut buffer_in).await?; @@ -175,11 +173,7 @@ impl DemoServer for StdDemo { } let lw = sftp_handler - .process( - &buffer_in[0..lr], - &mut incomplete_request_holder, - &mut buffer_out, - ) + .process(&buffer_in[0..lr], &mut buffer_out) .await?; if lw > 0 { stdio.write(&mut buffer_out[0..lw]).await?; diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index ee2019f4..0d667c01 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -13,8 +13,6 @@ pub use sftpserver::ReadReply; pub use sftpserver::SftpOpResult; pub use sftpserver::SftpServer; -pub use requestholder::RequestHolder; - pub use sftphandle::SftpHandler; pub use opaquefilehandle::{OpaqueFileHandle, OpaqueFileHandleManager, PathFinder}; diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs index af35e396..1042d2d2 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/requestholder.rs @@ -1,11 +1,11 @@ -use crate::{proto, sftperror, sftpsource::SftpSource}; +use crate::{proto, sftpsource::SftpSource}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; use sunset::sshwire::WireError; #[derive(Debug)] -pub(crate) enum RequestHolderError { +pub enum RequestHolderError { /// The slice to hold is too long NoRoom, /// The slice holder is keeping a slice already. Consideer cleaning @@ -283,7 +283,6 @@ impl<'a> RequestHolder<'a> { return Err(RequestHolderError::RanOut); } } - Ok(()) } /// Gets a reference to the slice that it is holding diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index a97d52fd..b957fc62 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -1,21 +1,37 @@ -use core::convert::From; +use crate::{StatusCode, requestholder::RequestHolderError}; -use log::warn; use sunset::Error as SunsetError; use sunset::sshwire::WireError; -use crate::{SftpOpResult, StatusCode, requestholder::RequestHolderError}; +use core::convert::From; +use log::warn; +// TODO: Use it more broadly where reasonable +/// Errors that are specific to this SFTP lib #[derive(Debug)] pub enum SftpError { + /// The SFTP server has not been initialised. No SFTP version has been + /// establish NotInitialized, + /// An `SSH_FXP_INIT` packet was received after the server was already + /// initialized AlreadyInitialized, + /// A packet could not be decoded as it was malformed MalformedPacket, + /// The server does not have an implementation for the current request. + /// Some possible causes are: + /// + /// - The request has not been handled by an [`crate::sftpserver::SftpServer`] + /// - Long request which its handling was not implemented NotSupported, + /// The [`crate::sftpserver::SftpServer`] failed doing an IO operation + FileServerError(StatusCode), + /// A RequestHolder instance throw an error. See [`crate::requestholder::RequestHolderError`] + RequestHolderError(RequestHolderError), + /// A variant containing a [`WireError`] WireError(WireError), - OperationError(StatusCode), + /// A variant containing a [`SunsetError`] SunsetError(SunsetError), - RequestHolderError(RequestHolderError), } impl From for SftpError { @@ -72,13 +88,17 @@ impl From for SunsetError { warn!("Casting error loosing information: {:?}", value); SunsetError::PacketWrong {} } - SftpError::OperationError(_) => { + SftpError::RequestHolderError(_) => { warn!("Casting error loosing information: {:?}", value); - SunsetError::PacketWrong {} + SunsetError::Bug + } + SftpError::FileServerError(_) => { + warn!("Casting error loosing information: {:?}", value); + SunsetError::Bug } - SftpError::RequestHolderError(request_holder_error) => SunsetError::Bug, } } } +/// result specific to this SFTP lib pub type SftpResult = Result; diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandle.rs index 17f19729..8ce0c20e 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandle.rs @@ -1,6 +1,6 @@ use crate::proto::{ - self, InitVersionLowest, ReqId, SFTP_FIELD_ID_INDEX, SFTP_MINIMUM_PACKET_LEN, - SFTP_VERSION, SftpNum, SftpPacket, Status, StatusCode, + self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, + SftpPacket, Status, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; use crate::sftperror::SftpResult; @@ -10,13 +10,13 @@ use crate::sftpsource::SftpSource; use crate::{OpaqueFileHandle, SftpError}; use sunset::Error as SunsetError; -use sunset::Error; use sunset::sshwire::{SSHSource, WireError, WireResult}; use core::{u32, usize}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; +/// FSM for handling sftp requests during `process()` #[derive(Default, Debug, PartialEq, Eq)] enum SftpHandleState { /// The handle is not initialized @@ -28,15 +28,20 @@ enum SftpHandleState { Fragmented(FragmentedRequestState), } +/// FSM subset to handle fragmented request as part of `SftpHandleState` #[derive(Debug, PartialEq, Eq)] enum FragmentedRequestState { - /// A request that is clipped needs to be process. It cannot be decoded and more bytes are needed + /// A request that is clipped needs to be process. It cannot be decoded + /// and more bytes are needed ProcessingClippedRequest, - /// A request, with a length over the incoming buffer capacity is being processed + /// A request, with a length over the incoming buffer capacity is being + /// processed ProcessingLongRequest, } -/// Used to keep record of a long SFTP Write request that does not fit in receiving buffer and requires processing in batches +// TODO: Generalize this to allow other request types +/// Used to keep record of a long SFTP Write request that does not fit in +/// receiving buffer and requires processing in batches #[derive(Debug)] pub struct PartialWriteRequestTracker { req_id: ReqId, @@ -46,6 +51,7 @@ pub struct PartialWriteRequestTracker { } impl PartialWriteRequestTracker { + /// Creates a new PartialWriteRequestTracker pub fn new( req_id: ReqId, obscure_file_handle: T, @@ -59,15 +65,18 @@ impl PartialWriteRequestTracker { remain_data_offset, }) } - + /// Returns the opaque file handle associated with the request + /// tracked pub fn get_opaque_file_handle(&self) -> T { self.obscure_file_handle.clone() } } -/// Process the raw buffers in and out from a subsystem channel decoding request and encoding responses +/// Process the raw buffers in and out from a subsystem channel decoding +/// request and encoding responses /// -/// It will delegate request to an `SftpServer` implemented by the library user taking into account the local system details. +/// It will delegate request to an `SftpServer` implemented by the library +/// user taking into account the local system details. pub struct SftpHandler<'a, T, S> where T: OpaqueFileHandle, @@ -76,14 +85,15 @@ where /// Holds the internal state if the SFTP handle state: SftpHandleState, - /// The local SFTP File server implementing the basic SFTP requests defined by `SftpServer` + /// The local SFTP File server implementing the basic SFTP requests + /// defined by `SftpServer` file_server: &'a mut S, - /// Once the client and the server have verified the agreed SFTP version the session is initialized - initialized: bool, - - /// Use to process SFTP Write packets that have been received partially and the remaining is expected in successive buffers + /// Use to process SFTP Write packets that have been received + /// partially and the remaining is expected in successive buffers partial_write_request_tracker: Option>, + + incomplete_request_holder: RequestHolder<'a>, } impl<'a, T, S> SftpHandler<'a, T, S> @@ -91,21 +101,39 @@ where T: OpaqueFileHandle, S: SftpServer<'a, T>, { - pub fn new(file_server: &'a mut S) -> Self { + /// Creates a new instance of the structure. + /// + /// Requires: + /// + /// - `file_server` (implementing `SftpServer`): to execute + /// the request in the local system + /// - `incomplete_request_buffer`: used to deal with fragmented + /// packets during `process()` + pub fn new( + file_server: &'a mut S, + incomplete_request_buffer: &'a mut [u8], + ) -> Self { SftpHandler { file_server, - initialized: false, partial_write_request_tracker: None, state: SftpHandleState::default(), + incomplete_request_holder: RequestHolder::new(incomplete_request_buffer), } } - /// Decodes the buffer_in request, process the request delegating operations to an Struct implementing SftpServer, - /// serializes an answer in buffer_out and **returns** the length used in buffer_out + /// Maintaining an internal status: + /// + /// - Decodes the buffer_in request + /// - Process the request delegating + /// operations to a `SftpServer` implementation + /// - Serializes an answer in buffer_out + /// + /// **Returns**: A result containing the number of bytes used in + /// `buffer_out` pub async fn process( &mut self, buffer_in: &[u8], - incomplete_request_holder: &mut RequestHolder<'_>, + // incomplete_request_holder: &mut RequestHolder<'_>, buffer_out: &mut [u8], ) -> SftpResult { let in_len = buffer_in.len(); @@ -134,7 +162,8 @@ where SftpHandleState::Fragmented(fragment_case) => { match fragment_case { FragmentedRequestState::ProcessingClippedRequest => { - if let Err(e) = incomplete_request_holder + if let Err(e) = self + .incomplete_request_holder .try_append_for_valid_request( // TODO: All your problems are here. Focus &buffer_in[buffer_in_remaining_index..], @@ -146,8 +175,9 @@ where "There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); - buffer_in_remaining_index += - incomplete_request_holder.appended(); + buffer_in_remaining_index += self + .incomplete_request_holder + .appended(); continue; } RequestHolderError::WireError( @@ -157,8 +187,9 @@ where "WIRE ERROR: There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); - buffer_in_remaining_index += - incomplete_request_holder.appended(); + buffer_in_remaining_index += self + .incomplete_request_holder + .appended(); continue; } RequestHolderError::NoRoom => { @@ -181,27 +212,36 @@ where ); } - let used = incomplete_request_holder.appended(); + let used = self.incomplete_request_holder.appended(); buffer_in_remaining_index += used; let mut source = SftpSource::new( - &incomplete_request_holder.try_get_ref()?, + &self.incomplete_request_holder.try_get_ref()?, ); trace!("Internal Source Content: {:?}", source); match SftpPacket::decode_request(&mut source) { Ok(request) => { - self.handle_general_request(&mut sink, request)?; - incomplete_request_holder.reset(); + Self::handle_general_request( + &mut self.file_server, + &mut sink, + request, + )?; + self.incomplete_request_holder.reset(); self.state = SftpHandleState::Idle; } Err(e) => match e { WireError::RanOut => { - match self - .handle_ran_out(&mut sink, &mut source) - { - Ok(_) => { - incomplete_request_holder.reset(); + match Self::handle_ran_out( + &mut self.file_server, + &mut sink, + &mut source, + ) { + Ok(holder) => { + self.partial_write_request_tracker = + Some(holder); + self.incomplete_request_holder + .reset(); self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); } Err(e) => match e { @@ -258,9 +298,13 @@ where let data_segment = // Fails!! source.dec_as_binstring(usable_data)?; - let data_segment_len = - u32::try_from(data_segment.0.len()) - .map_err(|e| SunsetError::Bug)?; + let data_segment_len = u32::try_from( + data_segment.0.len(), + ) + .map_err(|e| { + error!("Error casting data segment len to u32: {e}"); + SunsetError::Bug + })?; let current_write_offset = write_tracker.remain_data_offset; write_tracker.remain_data_offset += @@ -323,7 +367,7 @@ where SftpHandleState::Initializing => match sftp_packet { Ok(request) => { match request { - SftpPacket::Init(init_version_client) => { + SftpPacket::Init(_) => { let version = SftpPacket::Version(InitVersionLowest { version: SFTP_VERSION, @@ -353,7 +397,12 @@ where SftpHandleState::Idle => { match sftp_packet { Ok(request) => { - self.handle_general_request(&mut sink, request)? + // self.handle_general_request(&mut sink, request)? + Self::handle_general_request( + &mut self.file_server, + &mut sink, + request, + )?; } Err(e) => match e { WireError::RanOut => { @@ -362,27 +411,40 @@ where e ); - match self.handle_ran_out(&mut sink, &mut source) { - Ok(_) => { - self.state = + match Self::handle_ran_out( + &mut self.file_server, + &mut sink, + &mut source, + ) { + Ok(holder) => { + self.partial_write_request_tracker = + Some(holder); + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) - } - Err(e) => { - error!("Error handle_ran_out"); - match e { - SftpError::WireError(WireError::RanOut) => { - - let read = incomplete_request_holder + } + Err(e) => { + error!("Error handle_ran_out"); + match e { + SftpError::WireError( + WireError::RanOut, + ) => { + let read = self.incomplete_request_holder .try_hold( &buffer_in [buffer_in_remaining_index..], - )?; - buffer_in_remaining_index += read; + )?; + buffer_in_remaining_index += + read; self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); continue; } - _ => return (Err(SunsetError::Bug.into())), - }}, + _ => { + return Err( + SunsetError::Bug.into(), + ); + } + } + } }; } WireError::UnknownPacket { number: _ } => { @@ -416,26 +478,27 @@ where } fn handle_general_request( - &mut self, + file_server: &mut S, sink: &mut SftpSink<'_>, request: SftpPacket<'_>, ) -> Result<(), SftpError> where T: OpaqueFileHandle, { - Ok(match request { - SftpPacket::Init(init_version_client) => { - return Err(SftpError::MalformedPacket); + match request { + SftpPacket::Init(_) => { + error!("The Init packet is not a request but an initialization"); + return Err(SftpError::AlreadyInitialized); } SftpPacket::PathInfo(req_id, path_info) => { - let a_name = self.file_server.realpath(path_info.path.as_str()?)?; + let a_name = file_server.realpath(path_info.path.as_str()?)?; let response = SftpPacket::Name(req_id, a_name); response.encode_response(sink)?; } SftpPacket::Open(req_id, open) => { - match self.file_server.open(open.filename.as_str()?, &open.attrs) { + match file_server.open(open.filename.as_str()?, &open.attrs) { Ok(opaque_file_handle) => { let response = SftpPacket::Handle( req_id, @@ -453,7 +516,7 @@ where }; } SftpPacket::Write(req_id, write) => { - match self.file_server.write( + match file_server.write( &T::try_from(&write.handle)?, write.offset, write.data.as_ref(), @@ -466,7 +529,7 @@ where }; } SftpPacket::Close(req_id, close) => { - match self.file_server.close(&T::try_from(&close.handle)?) { + match file_server.close(&T::try_from(&close.handle)?) { Ok(_) => push_ok(req_id, sink)?, Err(e) => { error!("SFTP Close thrown: {:?}", e); @@ -478,29 +541,35 @@ where error!("Unsuported request type"); push_unsupported(ReqId(0), sink)?; } - }) + } + Ok(()) } + // TODO: Handle more long requests /// Handles long request that do not fit in the buffers and stores a tracker /// /// **WARNING:** Only `SSH_FXP_WRITE` has been implemented! /// fn handle_ran_out( - &mut self, + file_server: &mut S, sink: &mut SftpSink<'_>, source: &mut SftpSource<'_>, - ) -> SftpResult<()> { + ) -> SftpResult> { let packet_type = source.peak_packet_type()?; match packet_type { SftpNum::SSH_FXP_WRITE => { - debug!("about to decode packet partial write content. Source remaining = {:?}",source.remaining()); + debug!( + "about to decode packet partial write content. Source remaining = {:?}", + source.remaining() + ); let ( obscured_file_handle, req_id, offset, data_in_buffer, write_tracker, - ) = source.dec_packet_partial_write_content_and_get_tracker()?; + ) = source + .dec_packet_partial_write_content_and_get_tracker::()?; trace!( "obscured_file_handle = {:?}, req_id = {:?}, \ @@ -513,7 +582,7 @@ where write_tracker, ); - match self.file_server.write( + match file_server.write( &obscured_file_handle, offset, data_in_buffer.as_ref(), @@ -522,11 +591,13 @@ where debug!( "Storing a write tracker for a fragmented write request" ); - self.partial_write_request_tracker = Some(write_tracker); // TODO: This might belong to return value + return Ok(write_tracker); + // partial_write_request_tracker = Some(write_tracker); // TODO: This might belong to return value } Err(e) => { error!("SFTP write thrown: {:?}", e); push_general_failure(req_id, "error writing ", sink)?; + return Err(SftpError::FileServerError(e)); } }; } @@ -535,14 +606,10 @@ where "RanOut of Packet type could not be handled {:?}", packet_type ); - push_general_failure( - ReqId(u32::MAX), - "Unsupported Request: Too long", - sink, - )?; + return Err(SftpError::NotSupported); } }; - Ok(()) + // Ok(()) } } diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index f7f023ad..e1c75b55 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -5,7 +5,6 @@ use crate::proto::{ use crate::sftphandle::PartialWriteRequestTracker; use crate::{FileHandle, OpaqueFileHandle, SftpError, SftpResult}; -use sunset::error::RanOut; use sunset::sshwire::{BinString, SSHDecode, SSHSource, WireError, WireResult}; #[allow(unused_imports)] @@ -136,8 +135,4 @@ impl<'de> SftpSource<'de> { ) -> WireResult> { Ok(BinString(self.take(len)?)) } - - pub(crate) fn buffer_ref(&self) -> &[u8] { - self.buffer.clone() - } } From 930391a2b87ea3f6733b3847bcbdc598ceab7c04 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 14:09:28 +1000 Subject: [PATCH 306/393] WIP: sftphandle->sftphandler fixing sftperror statuscode casting --- sftp/src/lib.rs | 4 ++-- sftp/src/sftperror.rs | 2 +- sftp/src/{sftphandle.rs => sftphandler.rs} | 17 +++++++++-------- sftp/src/sftpsource.rs | 2 +- 4 files changed, 13 insertions(+), 12 deletions(-) rename sftp/src/{sftphandle.rs => sftphandler.rs} (98%) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 0d667c01..bfa1e99b 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,7 +1,7 @@ mod opaquefilehandle; mod proto; mod requestholder; -mod sftphandle; +mod sftphandler; mod sftpserver; mod sftpsink; mod sftpsource; @@ -13,7 +13,7 @@ pub use sftpserver::ReadReply; pub use sftpserver::SftpOpResult; pub use sftpserver::SftpServer; -pub use sftphandle::SftpHandler; +pub use sftphandler::SftpHandler; pub use opaquefilehandle::{OpaqueFileHandle, OpaqueFileHandleManager, PathFinder}; diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index b957fc62..13d6c333 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -48,7 +48,7 @@ impl From for SftpError { impl From for SftpError { fn from(value: StatusCode) -> Self { - SftpError::OperationError(value) + SftpError::FileServerError(value) } } diff --git a/sftp/src/sftphandle.rs b/sftp/src/sftphandler.rs similarity index 98% rename from sftp/src/sftphandle.rs rename to sftp/src/sftphandler.rs index 8ce0c20e..759db2a1 100644 --- a/sftp/src/sftphandle.rs +++ b/sftp/src/sftphandler.rs @@ -16,7 +16,7 @@ use core::{u32, usize}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -/// FSM for handling sftp requests during `process()` +/// FSM for handling sftp requests during [`SftpHandler::process`] #[derive(Default, Debug, PartialEq, Eq)] enum SftpHandleState { /// The handle is not initialized @@ -28,7 +28,7 @@ enum SftpHandleState { Fragmented(FragmentedRequestState), } -/// FSM subset to handle fragmented request as part of `SftpHandleState` +/// FSM subset to handle fragmented request as part of [`SftpHandleState`] #[derive(Debug, PartialEq, Eq)] enum FragmentedRequestState { /// A request that is clipped needs to be process. It cannot be decoded @@ -51,7 +51,7 @@ pub struct PartialWriteRequestTracker { } impl PartialWriteRequestTracker { - /// Creates a new PartialWriteRequestTracker + /// Creates a new [`PartialWriteRequestTracker`] pub fn new( req_id: ReqId, obscure_file_handle: T, @@ -75,7 +75,8 @@ impl PartialWriteRequestTracker { /// Process the raw buffers in and out from a subsystem channel decoding /// request and encoding responses /// -/// It will delegate request to an `SftpServer` implemented by the library +/// It will delegate request to an [`crate::sftpserver::SftpServer`] +/// implemented by the library /// user taking into account the local system details. pub struct SftpHandler<'a, T, S> where @@ -86,7 +87,7 @@ where state: SftpHandleState, /// The local SFTP File server implementing the basic SFTP requests - /// defined by `SftpServer` + /// defined by [`crate::sftpserver::SftpServer`] file_server: &'a mut S, /// Use to process SFTP Write packets that have been received @@ -105,10 +106,10 @@ where /// /// Requires: /// - /// - `file_server` (implementing `SftpServer`): to execute + /// - `file_server` (implementing [`crate::sftpserver::SftpServer`] ): to execute /// the request in the local system /// - `incomplete_request_buffer`: used to deal with fragmented - /// packets during `process()` + /// packets during [`SftpHandler::process`] pub fn new( file_server: &'a mut S, incomplete_request_buffer: &'a mut [u8], @@ -125,7 +126,7 @@ where /// /// - Decodes the buffer_in request /// - Process the request delegating - /// operations to a `SftpServer` implementation + /// operations to a [`SftpHandler::process`] implementation /// - Serializes an answer in buffer_out /// /// **Returns**: A result containing the number of bytes used in diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index e1c75b55..94142c78 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -2,7 +2,7 @@ use crate::proto::{ ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, SFTP_MINIMUM_PACKET_LEN, SFTP_WRITE_REQID_INDEX, SftpNum, }; -use crate::sftphandle::PartialWriteRequestTracker; +use crate::sftphandler::PartialWriteRequestTracker; use crate::{FileHandle, OpaqueFileHandle, SftpError, SftpResult}; use sunset::sshwire::{BinString, SSHDecode, SSHSource, WireError, WireResult}; From d5b3e01612eb55e3e4480b0a07b3c0f6559db85f Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 14:17:30 +1000 Subject: [PATCH 307/393] WIP: Removing dead code and adding comments --- sftp/src/lib.rs | 3 +++ sftp/src/requestholder.rs | 46 +-------------------------------------- sftp/src/sftpserver.rs | 7 ++++-- sftp/src/sftpsink.rs | 15 +++++++++++-- sftp/src/sftpsource.rs | 40 +++++++++++++++++++++------------- 5 files changed, 47 insertions(+), 64 deletions(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index bfa1e99b..1ed6689e 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,3 +1,6 @@ +#![forbid(unsafe_code)] +#![warn(missing_docs)] + mod opaquefilehandle; mod proto; mod requestholder; diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs index 1042d2d2..ed0abf4e 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/requestholder.rs @@ -12,8 +12,6 @@ pub enum RequestHolderError { Busy, /// The slice holder is empty Empty, - /// The instance has been invalidated - Invalid, /// There is not enough data in the slice we are trying to add. we need more data RanOut, /// WireError @@ -50,8 +48,6 @@ pub(crate) type RequestHolderResult = Result; /// /// - `reset`: reset counters and flags to allow `try_hold` a new request /// -/// - **OR** `invalidate`: return the reference to the slice provided in `new` -/// and mark the structure as invalid. At this point it should be disposed #[derive(Debug)] pub struct RequestHolder<'a> { /// The buffer used to contain the data for the request @@ -60,8 +56,6 @@ pub struct RequestHolder<'a> { buffer_fill_index: usize, /// Number of bytes appended in a previous `try_hold` or `try_append_for_valid_request` slice appended: usize, - /// Marks the structure as invalid - invalid: bool, /// Used to mark when the structure is holding data busy: bool, } @@ -73,7 +67,6 @@ impl<'a> RequestHolder<'a> { RequestHolder { buffer: buffer, buffer_fill_index: 0, - invalid: false, // TODO: Remove if invalidate() is removed busy: false, appended: 0, } @@ -91,15 +84,10 @@ impl<'a> RequestHolder<'a> { /// - Ok(usize): the number of bytes read from the slice /// /// - `Err(Busy)`: If there has been a call to `try_hold` without a call to `reset` - /// - /// - `Err(Invalid)`: If the structure has been marked as invalid previously pub fn try_hold(&mut self, slice: &[u8]) -> RequestHolderResult { if self.busy { return Err(RequestHolderError::Busy); } - if self.invalid { - return Err(RequestHolderError::Invalid); - } self.busy = true; self.try_append_slice(slice)?; @@ -115,26 +103,10 @@ impl<'a> RequestHolder<'a> { /// Will not clear the previous data from the buffer. pub fn reset(&mut self) -> () { self.busy = false; - self.invalid = false; self.buffer_fill_index = 0; self.appended = 0; } - // TODO: Remove it if it is not used - /// Invalidates the current instance and returns its original buffer. Does not erase previous data - pub fn invalidate(&mut self) -> RequestHolderResult<&[u8]> { - if !self.busy { - return Err(RequestHolderError::Empty); - } - if self.invalid { - return Err(RequestHolderError::Invalid); - } - - self.invalid = true; - // self.buffer_fill_index = 0; - Ok(&self.buffer) - } - /// Using the content of the `RequestHolder` tries to find a valid /// SFTP request appending from slice into the internal buffer to /// form a valid request. @@ -149,8 +121,6 @@ impl<'a> RequestHolder<'a> { /// /// - `Err(NoRoom)`: The internal buffer is full but there is not a full valid request in the buffer /// - /// - `Err(Invalid)`: If the structure has been marked as invalid previously - /// /// - `Err(Empty)`: If the structure has not been loaded with `try_hold` /// /// - `Err(Bug)`: An unexpected condition arises @@ -167,11 +137,6 @@ impl<'a> RequestHolder<'a> { slice.len() ); - if self.invalid { - error!("Request Holder is invalid"); - return Err(RequestHolderError::Invalid); - } - if !self.busy { error!("Request Holder is not busy"); return Err(RequestHolderError::Empty); @@ -287,10 +252,6 @@ impl<'a> RequestHolder<'a> { /// Gets a reference to the slice that it is holding pub fn try_get_ref(&self) -> RequestHolderResult<&[u8]> { - if self.invalid { - return Err(RequestHolderError::Invalid); - } - if self.busy { debug!( "Returning reference to: {:?}", @@ -306,6 +267,7 @@ impl<'a> RequestHolder<'a> { self.buffer_fill_index == self.buffer.len() } + #[allow(unused)] /// Returns true if it has a slice in its buffer pub fn is_busy(&self) -> bool { self.busy @@ -325,8 +287,6 @@ impl<'a> RequestHolder<'a> { /// /// - `Ok(())`: the slice was appended /// - /// - `Err(Invalid)`: If the structure has been marked as invalid previously - /// /// - `Err(Empty)`: If the structure has not been loaded with `try_hold` /// /// - `Err(NoRoom)`: The internal buffer is full but there is not a full valid request in the buffer @@ -339,10 +299,6 @@ impl<'a> RequestHolder<'a> { return Err(RequestHolderError::Empty); } - if self.invalid { - return Err(RequestHolderError::Invalid); - } - let in_len = slice.len(); if in_len > self.remaining_len() { return Err(RequestHolderError::NoRoom); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 201c7eb3..22f5bffb 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -5,6 +5,7 @@ use crate::{ use core::marker::PhantomData; +/// Result used to store the result of an Sftp Operation pub type SftpOpResult = core::result::Result; /// All trait functions are optional in the SFTP protocol. @@ -31,7 +32,7 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - + /// Reads from a file that has previously being opened for reading fn read( &mut self, opaque_file_handle: &T, @@ -45,7 +46,7 @@ where ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - + /// Writes to a file that has previously being opened for writing fn write( &mut self, opaque_file_handle: &T, @@ -61,11 +62,13 @@ where Ok(()) } + /// Opens a directory fn opendir(&mut self, dir: &str) -> SftpOpResult { log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } + /// Reads the list of items in a directory fn readdir( &mut self, opaque_file_handle: &T, diff --git a/sftp/src/sftpsink.rs b/sftp/src/sftpsink.rs index f3bb85c3..ce55d265 100644 --- a/sftp/src/sftpsink.rs +++ b/sftp/src/sftpsink.rs @@ -5,6 +5,11 @@ use sunset::sshwire::{SSHSink, WireError}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; +/// A implementation fo [`SSHSink`] that observes some constraints for +/// SFTP packets +/// +/// **Important**: It needs to be [`SftpSink::finalize`] to add the packet +/// len #[derive(Default)] pub struct SftpSink<'g> { pub buffer: &'g mut [u8], @@ -12,13 +17,19 @@ pub struct SftpSink<'g> { } impl<'g> SftpSink<'g> { + /// Initializes the Sink, with the particularity that it will leave + /// [`crate::proto::SFTP_FIELD_LEN_LENGTH`] bytes empty at the + /// start of the buffer that will contain the total packet length + /// once the [`SftpSink::finalize`] method is called pub fn new(s: &'g mut [u8]) -> Self { SftpSink { buffer: s, index: SFTP_FIELD_LEN_LENGTH } } - /// Finalise the buffer by prepending the payload size and returning + /// Finalise the buffer by prepending the packet length field, + /// excluding the field itself. /// - /// Returns the final index in the buffer as a reference for the space used + /// **Returns** the final index in the buffer as a reference of the + /// space used pub fn finalize(&mut self) -> usize { if self.index <= SFTP_FIELD_LEN_LENGTH { warn!("SftpSink trying to terminate it before pushing data"); diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 94142c78..b1f0a2c0 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -10,15 +10,15 @@ use sunset::sshwire::{BinString, SSHDecode, SSHSource, WireError, WireResult}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -/// SftpSource implements SSHSource and also extra functions to handle some challenges with long SFTP packets in constrained environments +/// SftpSource implements [`SSHSource`] and also extra functions to handle +/// some challenges related to long SFTP packets in constrained environments #[derive(Default, Debug)] pub struct SftpSource<'de> { - pub buffer: &'de [u8], - pub index: usize, + buffer: &'de [u8], + index: usize, } impl<'de> SSHSource<'de> for SftpSource<'de> { - // Original take fn take(&mut self, len: usize) -> sunset::sshwire::WireResult<&'de [u8]> { if len + self.index > self.buffer.len() { return Err(WireError::RanOut); @@ -43,15 +43,19 @@ impl<'de> SSHSource<'de> for SftpSource<'de> { } impl<'de> SftpSource<'de> { + /// Creates a new [`SftpSource`] referencing a buffer pub fn new(buffer: &'de [u8]) -> Self { SftpSource { buffer: buffer, index: 0 } } - /// Peaks the buffer for packet type. This does not advance the reading index + /// Peaks the buffer for packet type [`SftpNum`]. This does not advance + /// the reading index /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// Useful to observe the packet fields in special conditions where a + /// `dec(s)` would fail /// - /// **Warning**: will only work in well formed packets, in other case the result will contain garbage + /// **Warning**: will only work in well formed packets, in other case + /// the result will contain garbage pub(crate) fn peak_packet_type(&self) -> WireResult { if self.buffer.len() < SFTP_FIELD_ID_INDEX + 1 { Err(WireError::RanOut) @@ -62,9 +66,11 @@ impl<'de> SftpSource<'de> { /// Peaks the buffer for packet length. This does not advance the reading index /// - /// Useful to observe the packet fields in special conditions where a `dec(s)` would fail + /// Useful to observe the packet fields in special conditions where a `dec(s)` + /// would fail /// - /// **Warning**: will only work in well formed packets, in other case the result will contain garbage + /// **Warning**: will only work in well formed packets, in other case the result + /// will contain garbage pub(crate) fn peak_packet_len(&self) -> WireResult { if self.buffer.len() < SFTP_FIELD_LEN_INDEX + SFTP_FIELD_LEN_LENGTH { Err(WireError::RanOut) @@ -78,11 +84,13 @@ impl<'de> SftpSource<'de> { } } - /// Assuming that the buffer contains a Write request packet initial bytes and not its totality, - /// extracts a partial version of the write request and a Write request tracker to handle and a - /// tracker to continue processing subsequent portions of the request from a SftpSource + /// Assuming that the buffer contains a [`proto::Write`] request packet initial + /// bytes and not its totality, extracts a partial version of the write request + /// and a Write request tracker to handle and a tracker to continue processing + /// subsequent portions of the request from a SftpSource /// - /// **Warning**: will only work in well formed write packets, in other case the result will contain garbage + /// **Warning**: will only work in well formed write packets, in other case + /// the result will contain garbage pub(crate) fn dec_packet_partial_write_content_and_get_tracker< T: OpaqueFileHandle, >( @@ -126,9 +134,11 @@ impl<'de> SftpSource<'de> { Ok((obscured_file_handle, req_id, offset, data_in_buffer, write_tracker)) } - /// Used to decode a slice of SSHSource as a single BinString + /// Used to decode a slice of [`SSHSource`] as a single BinString /// - /// It will not use the first four bytes as u32 for length, instead it will use the length of the data received and use it to set the length of the returned BinString. + /// It will not use the first four bytes as u32 for length, instead + /// it will use the length of the data received and use it to set the + /// length of the returned BinString. pub(crate) fn dec_as_binstring( &mut self, len: usize, From 6eecbcb92aaae45e487ffdf1d9795c6d6ea78aff Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 15:10:03 +1000 Subject: [PATCH 308/393] TODO: Use heapless::Vec instead of Vec for Name as it breaks the no_std premisses --- sftp/src/proto.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 0ed4bea0..4acb8125 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -135,6 +135,10 @@ pub struct NameEntry<'a> { pub attrs: Attrs, } +// TODO: Will a Vector be an issue for no_std? +// Maybe we should migrate this to heapless::Vec and let the user decide +// the number of elements via features flags? +/// A collection of [`NameEntry`] #[derive(Debug)] pub struct Name<'a>(pub Vec>); From bed2a083c377866ecee7c1ac7a9c5c68468a7dc3 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 15:54:58 +1000 Subject: [PATCH 309/393] WIP: fixed bad tag ids --- sftp/src/sftphandler.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sftp/src/sftphandler.rs b/sftp/src/sftphandler.rs index 759db2a1..3b3a5855 100644 --- a/sftp/src/sftphandler.rs +++ b/sftp/src/sftphandler.rs @@ -621,7 +621,7 @@ fn push_ok(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireError> { Status { code: StatusCode::SSH_FX_OK, message: "".into(), - lang: "EN".into(), + lang: "en-US".into(), }, ); trace!("Pushing an OK status message: {:?}", response); @@ -639,7 +639,7 @@ fn push_unsupported( Status { code: StatusCode::SSH_FX_OP_UNSUPPORTED, message: "Not implemented".into(), - lang: "EN".into(), + lang: "en-US".into(), }, ); debug!("Pushing a unsupported status message: {:?}", response); @@ -658,7 +658,7 @@ fn push_general_failure( Status { code: StatusCode::SSH_FX_FAILURE, message: msg.into(), - lang: "EN".into(), + lang: "en-US".into(), }, ); debug!("Pushing a general failure status message: {:?}", response); From 268086b4464f6f4b59fb7fab9155ff13e182c3dc Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 15:55:42 +1000 Subject: [PATCH 310/393] WIP: More docs --- sftp/src/proto.rs | 58 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 4acb8125..bb04baf0 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -37,6 +37,7 @@ pub const SFTP_WRITE_REQID_INDEX: usize = 5; // pub const SFTP_WRITE_HANDLE_INDEX: usize = 9; // TODO is utf8 enough, or does this need to be an opaque binstring? +/// See [SSH_FXP_NAME in Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) #[derive(Debug, SSHEncode, SSHDecode)] pub struct Filename<'a>(TextString<'a>); @@ -46,12 +47,16 @@ impl<'a> From<&'a str> for Filename<'a> { } } +// TODO: standardize the encoding of filenames as str impl<'a> Filename<'a> { + /// pub fn as_str(&self) -> Result<&'a str, WireError> { core::str::from_utf8(self.0.0).map_err(|_| WireError::BadString) } } +/// An opaque handle that is used by the server to identify an open +/// file or folder. #[derive(Debug, Clone, Copy, PartialEq, Eq, SSHEncode, SSHDecode)] pub struct FileHandle<'a>(pub BinString<'a>); @@ -74,71 +79,107 @@ pub struct InitVersionLowest { // TODO variable number of ExtPair } +/// Used for `ssh_fxp_open` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Open<'a> { + /// The relative or absolute path of the file to be open pub filename: Filename<'a>, + /// File [permissions flags](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3) pub pflags: u32, + /// Initial attributes for the file pub attrs: Attrs, } +/// Used for `ssh_fxp_close` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Close<'a> { + /// An opaque handle that is used by the server to identify an open + /// file or folder to be closed. pub handle: FileHandle<'a>, } +/// Used for `ssh_fxp_read` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Read<'a> { + /// An opaque handle that is used by the server to identify an open + /// file or folder. pub handle: FileHandle<'a>, + /// The offset for the read operation pub offset: u64, + /// The number of bytes to be retrieved pub len: u32, } +/// Used for `ssh_fxp_write` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Write<'a> { + /// An opaque handle that is used by the server to identify an open + /// file or folder. pub handle: FileHandle<'a>, + /// The offset for the read operation pub offset: u64, + pub data: BinString<'a>, } // Responses +/// Used for `ssh_fxp_realpath` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.11). #[derive(Debug, SSHEncode, SSHDecode)] pub struct PathInfo<'a> { + /// The path pub path: TextString<'a>, } +/// Used for `ssh_fxp_status` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Status<'a> { + /// See [`StatusCode`] for possible codes pub code: StatusCode, + /// An extra message pub message: TextString<'a>, + /// A language tag as defined by [Tags for the Identification of Languages](https://datatracker.ietf.org/doc/html/rfc1766) pub lang: TextString<'a>, } - +/// Used for `ssh_fxp_handle` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7). #[derive(Debug, Clone, Copy, PartialEq, Eq, SSHEncode, SSHDecode)] pub struct Handle<'a> { + /// An opaque handle that is used by the server to identify an open + /// file or folder. pub handle: FileHandle<'a>, } +/// Used for `ssh_fxp_data` [responses](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Data<'a> { + /// Handle for the file referred pub handle: FileHandle<'a>, + /// Offset in the data read pub offset: u64, + /// raw data pub data: BinString<'a>, } +/// Struct to hold `SSH_FXP_NAME` response. +/// See [SSH_FXP_NAME in Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) #[derive(Debug, SSHEncode, SSHDecode)] pub struct NameEntry<'a> { + /// Is a file name being returned pub filename: Filename<'a>, /// longname is an undefined text line like "ls -l", /// SHOULD NOT be used. pub _longname: Filename<'a>, + /// Attributes for the file entry + /// + /// See [File Attributes](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-5) + /// for more information. pub attrs: Attrs, } // TODO: Will a Vector be an issue for no_std? // Maybe we should migrate this to heapless::Vec and let the user decide // the number of elements via features flags? -/// A collection of [`NameEntry`] +/// A collection of [`NameEntry`] used for [ssh_fxp_name responses](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7). #[derive(Debug)] pub struct Name<'a>(pub Vec>); @@ -183,9 +224,10 @@ pub struct ResponseAttributes { #[derive(Debug, SSHEncode, SSHDecode, Clone, Copy)] pub struct ReqId(pub u32); +/// For more information see [Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) #[derive(Debug, FromPrimitive, SSHEncode)] #[repr(u32)] -#[allow(non_camel_case_types)] +#[allow(non_camel_case_types, missing_docs)] pub enum StatusCode { #[sshwire(variant = "ssh_fx_ok")] SSH_FX_OK = 0, @@ -226,9 +268,12 @@ pub struct ExtPair<'a> { } /// Files attributes to describe Files as SFTP v3 specification +/// +/// See [File Attributes](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-5) +/// for more information. +#[allow(missing_docs)] #[derive(Debug, Default)] pub struct Attrs { - // flags: u32, defines used attributes pub size: Option, pub uid: Option, pub gid: Option, @@ -239,6 +284,7 @@ pub struct Attrs { // TODO extensions } +/// For more information see [File Attributes](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-5) #[repr(u32)] #[allow(non_camel_case_types)] pub enum AttrsFlags { @@ -263,6 +309,10 @@ impl core::ops::BitAnd for u32 { } impl Attrs { + /// Obtains the flags for the values stored in the [`Attrs`] struct. + /// + /// See [File Attributes](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-5) + /// for more information. pub fn flags(&self) -> u32 { let mut flags: u32 = 0; if self.size.is_some() { From 94afb1f0e1f6022b20add82d90b78961f0f89a03 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 16:13:38 +1000 Subject: [PATCH 311/393] WIP: Refactoring SFTP lib and added comments from flat structure to a more conceptual structure Also added a brief header doc to show what is the intent and the status of the module. --- demo/sftp/std/src/demofilehandlemanager.rs | 5 +-- demo/sftp/std/src/demoopaquefilehandle.rs | 3 +- demo/sftp/std/src/demosftpserver.rs | 7 ++- demo/sftp/std/src/main.rs | 6 +-- sftp/src/asyncsftpserver.rs | 2 +- sftp/src/lib.rs | 51 +++++++++++++++------- sftp/src/opaquefilehandle.rs | 2 +- sftp/src/proto.rs | 22 ++++++---- sftp/src/requestholder.rs | 2 - sftp/src/sftperror.rs | 5 ++- sftp/src/sftphandler.rs | 12 +++-- sftp/src/sftpserver.rs | 8 ++-- sftp/src/sftpsource.rs | 4 +- 13 files changed, 77 insertions(+), 52 deletions(-) diff --git a/demo/sftp/std/src/demofilehandlemanager.rs b/demo/sftp/std/src/demofilehandlemanager.rs index e7bae95a..feaa126f 100644 --- a/demo/sftp/std/src/demofilehandlemanager.rs +++ b/demo/sftp/std/src/demofilehandlemanager.rs @@ -1,6 +1,5 @@ -use sunset_sftp::{ - OpaqueFileHandle, OpaqueFileHandleManager, PathFinder, StatusCode, -}; +use sunset_sftp::handles::{OpaqueFileHandle, OpaqueFileHandleManager, PathFinder}; +use sunset_sftp::protocol::StatusCode; use std::collections::HashMap; // Not enforced. Only for std. For no_std environments other solutions can be used to store Key, Value diff --git a/demo/sftp/std/src/demoopaquefilehandle.rs b/demo/sftp/std/src/demoopaquefilehandle.rs index b7a1b92c..67c2fc6b 100644 --- a/demo/sftp/std/src/demoopaquefilehandle.rs +++ b/demo/sftp/std/src/demoopaquefilehandle.rs @@ -1,4 +1,5 @@ -use sunset_sftp::{FileHandle, OpaqueFileHandle}; +use sunset_sftp::handles::OpaqueFileHandle; +use sunset_sftp::protocol::FileHandle; use sunset::sshwire::{BinString, WireError}; diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 79591175..8ad893c5 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -3,10 +3,9 @@ use crate::{ demoopaquefilehandle::DemoOpaqueFileHandle, }; -use sunset_sftp::{ - Attrs, DirReply, Filename, Name, NameEntry, OpaqueFileHandleManager, PathFinder, - ReadReply, SftpOpResult, SftpServer, StatusCode, -}; +use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; +use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; +use sunset_sftp::server::{DirReply, ReadReply, SftpOpResult, SftpServer}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 74403ad6..51d4f24f 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -148,10 +148,10 @@ impl DemoServer for StdDemo { info!("SFTP loop has received a channel handle {:?}", ch.num()); - // TODO: Do some research to find reasonable default buffer lengths + // TODO Do some research to find reasonable default buffer lengths let mut buffer_in = [0u8; 512]; let mut buffer_out = [0u8; 384]; - let mut incomplete_request_buffer = [0u8; 128]; // TODO: Find a non arbitrary length + let mut incomplete_request_buffer = [0u8; 128]; // TODO Find a non arbitrary length match { let mut stdio = serv.stdio(ch).await?; @@ -209,7 +209,7 @@ impl DemoServer for StdDemo { } } -// TODO: pool_size should be NUM_LISTENERS but needs a literal +// TODO pool_size should be NUM_LISTENERS but needs a literal #[embassy_executor::task(pool_size = 4)] async fn listen( stack: Stack<'static>, diff --git a/sftp/src/asyncsftpserver.rs b/sftp/src/asyncsftpserver.rs index b2bd93e8..33458479 100644 --- a/sftp/src/asyncsftpserver.rs +++ b/sftp/src/asyncsftpserver.rs @@ -80,7 +80,7 @@ impl<'g, 'a> DirReply<'g, 'a> { pub async fn reply(self, data: &[u8]) {} } -// TODO: Implement correct Channel Out +// TODO Implement correct Channel Out pub struct ChanOut<'g, 'a> { _phantom_g: PhantomData<&'g ()>, _phantom_a: PhantomData<&'a ()>, diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 1ed6689e..2fc6465b 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,31 +1,52 @@ +//! SFTP (SSH File Transfer Protocol) implementation extending [`sunset`]. +//! +//! Partially Implements SFTP v3 as defined in [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02). +//! +//! **Work in Progress**: Currently focuses on file upload operations. +//! Long packets for other request types and additional SFTP operations are not yet implemented. +//! `no_std` compatibility is intended but not yet complete. +//! +//! See example usage in the `demo/sftd/std` directory. + #![forbid(unsafe_code)] #![warn(missing_docs)] mod opaquefilehandle; mod proto; mod requestholder; +mod sftperror; mod sftphandler; mod sftpserver; mod sftpsink; mod sftpsource; -mod sftperror; +pub use sftphandler::SftpHandler; -pub use sftpserver::DirReply; -pub use sftpserver::ReadReply; -pub use sftpserver::SftpOpResult; -pub use sftpserver::SftpServer; +pub mod server { -pub use sftphandler::SftpHandler; + pub use crate::sftpserver::DirReply; + pub use crate::sftpserver::ReadReply; + pub use crate::sftpserver::SftpOpResult; + pub use crate::sftpserver::SftpServer; +} -pub use opaquefilehandle::{OpaqueFileHandle, OpaqueFileHandleManager, PathFinder}; +pub mod handles { + pub use crate::opaquefilehandle::OpaqueFileHandle; + pub use crate::opaquefilehandle::OpaqueFileHandleManager; + pub use crate::opaquefilehandle::PathFinder; +} -pub use sftperror::{SftpError, SftpResult}; +pub mod protocol { + pub use crate::proto::Attrs; + pub use crate::proto::FileHandle; + pub use crate::proto::Filename; + pub use crate::proto::Name; + pub use crate::proto::NameEntry; + pub use crate::proto::PathInfo; + pub use crate::proto::StatusCode; +} -pub use proto::Attrs; -pub use proto::FileHandle; -pub use proto::Filename; -pub use proto::Name; -pub use proto::NameEntry; -pub use proto::PathInfo; -pub use proto::StatusCode; +pub mod error { + pub use crate::sftperror::SftpError; + pub use crate::sftperror::SftpResult; +} diff --git a/sftp/src/opaquefilehandle.rs b/sftp/src/opaquefilehandle.rs index d96114a6..18f0662b 100644 --- a/sftp/src/opaquefilehandle.rs +++ b/sftp/src/opaquefilehandle.rs @@ -1,4 +1,4 @@ -use crate::FileHandle; +use crate::protocol::FileHandle; use sunset::sshwire::WireResult; diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index bb04baf0..cd606478 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -47,7 +47,7 @@ impl<'a> From<&'a str> for Filename<'a> { } } -// TODO: standardize the encoding of filenames as str +// TODO standardize the encoding of filenames as str impl<'a> Filename<'a> { /// pub fn as_str(&self) -> Result<&'a str, WireError> { @@ -176,7 +176,7 @@ pub struct NameEntry<'a> { pub attrs: Attrs, } -// TODO: Will a Vector be an issue for no_std? +// TODO Will a Vector be an issue for no_std? // Maybe we should migrate this to heapless::Vec and let the user decide // the number of elements via features flags? /// A collection of [`NameEntry`] used for [ssh_fxp_name responses](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7). @@ -327,7 +327,7 @@ impl Attrs { if self.atime.is_some() || self.mtime.is_some() { flags += AttrsFlags::SSH_FILEXFER_ATTR_ACMODTIME } - // TODO: Implement extensions + // TODO Implement extensions // if self.ext_count.is_some() { // flags += AttrsFlags::SSH_FILEXFER_ATTR_EXTENDED // } @@ -359,7 +359,7 @@ impl SSHEncode for Attrs { if let Some(value) = self.mtime.as_ref() { value.enc(s)? } - // TODO: Implement extensions + // TODO Implement extensions // if let Some(value) = self.ext_count.as_ref() { value.enc(s)? } Ok(()) @@ -387,7 +387,7 @@ impl<'de> SSHDecode<'de> for Attrs { attrs.atime = Some(u32::dec(s)?); attrs.mtime = Some(u32::dec(s)?); } - // TODO: Implement extensions + // TODO Implement extensions // if flags & AttrsFlags::SSH_FILEXFER_ATTR_EXTENDED != 0{ Ok(attrs) @@ -618,6 +618,8 @@ macro_rules! sftpmessages { } } + // TODO Maybe change WireResult -> SftpResult and SSHSink to SftpSink? + // This way I have more internal details and can return a Error::bug() if required /// Encode a request. /// /// Used by a SFTP client. Does not include the length field. @@ -625,7 +627,7 @@ macro_rules! sftpmessages { if !self.sftp_num().is_request() { return Err(WireError::PacketWrong) // return Err(Error::bug()) - // TODO: I understand that it would be a bad call of encode_response and + // I understand that it would be a bad call of encode_response and // therefore a bug, bug Error::bug() is not compatible with WireResult } @@ -637,6 +639,8 @@ macro_rules! sftpmessages { self.enc(s) } + // TODO Maybe change WireResult -> SftpResult and SSHSource to SftpSource? + // This way I have more internal details and can return a more appropriate error if required /// Decode a response. /// /// Used by a SFTP client. Does not include the length field. @@ -651,7 +655,7 @@ macro_rules! sftpmessages { if !num.is_response() { return Err(WireError::PacketWrong) // return error::SSHProto.fail(); - // TODO: Not an error in the SSHProtocol rather the SFTP Protocol. + // Not an error in the SSHProtocol rather the SFTP Protocol. } let id = ReqId(u32::dec(s)?); @@ -690,6 +694,8 @@ macro_rules! sftpmessages { } } + // TODO Maybe change WireResult -> SftpResult and SSHSink to SftpSink? + // This way I have more internal details and can return a Error::bug() if required /// Encode a response. /// /// Used by a SFTP server. Does not include the length field. @@ -700,7 +706,7 @@ macro_rules! sftpmessages { if !self.sftp_num().is_response() { return Err(WireError::PacketWrong) // return Err(Error::bug()) - // TODO: I understand that it would be a bad call of encode_response and + // I understand that it would be a bad call of encode_response and // therefore a bug, bug Error::bug() is not compatible with WireResult } diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs index ed0abf4e..e1427fc2 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/requestholder.rs @@ -212,7 +212,6 @@ impl<'a> RequestHolder<'a> { self.buffer_fill_index - proto::SFTP_FIELD_LEN_LENGTH + remaining_packet_len ); - // TODO: Fix the mess with the logic and the indexes to address the slice. IT IS PANICKING if remaining_packet_len <= self.remaining_len() { // We have all the remaining packet bytes in the slice and fits in the buffer @@ -321,7 +320,6 @@ impl<'a> RequestHolder<'a> { /// Returns the number of bytes unused at the end of the buffer, /// this is, the remaining length fn remaining_len(&self) -> usize { - // self.buffer.len() - self.buffer_fill_index - 1 // TODO: Off by one? self.buffer.len() - self.buffer_fill_index } } diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index 13d6c333..134e716c 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -1,12 +1,13 @@ -use crate::{StatusCode, requestholder::RequestHolderError}; +use crate::protocol::StatusCode; +use crate::requestholder::RequestHolderError; use sunset::Error as SunsetError; use sunset::sshwire::WireError; use core::convert::From; use log::warn; -// TODO: Use it more broadly where reasonable +// TODO Use it more broadly where reasonable /// Errors that are specific to this SFTP lib #[derive(Debug)] pub enum SftpError { diff --git a/sftp/src/sftphandler.rs b/sftp/src/sftphandler.rs index 3b3a5855..a839919a 100644 --- a/sftp/src/sftphandler.rs +++ b/sftp/src/sftphandler.rs @@ -1,3 +1,5 @@ +use crate::error::SftpError; +use crate::handles::OpaqueFileHandle; use crate::proto::{ self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, SftpPacket, Status, StatusCode, @@ -7,7 +9,6 @@ use crate::sftperror::SftpResult; use crate::sftpserver::SftpServer; use crate::sftpsink::SftpSink; use crate::sftpsource::SftpSource; -use crate::{OpaqueFileHandle, SftpError}; use sunset::Error as SunsetError; use sunset::sshwire::{SSHSource, WireError, WireResult}; @@ -39,7 +40,7 @@ enum FragmentedRequestState { ProcessingLongRequest, } -// TODO: Generalize this to allow other request types +// TODO Generalize this to allow other request types /// Used to keep record of a long SFTP Write request that does not fit in /// receiving buffer and requires processing in batches #[derive(Debug)] @@ -166,7 +167,6 @@ where if let Err(e) = self .incomplete_request_holder .try_append_for_valid_request( - // TODO: All your problems are here. Focus &buffer_in[buffer_in_remaining_index..], ) { @@ -293,8 +293,7 @@ where let usable_data = source .remaining() - .min(write_tracker.remain_data_len as usize); // TODO: Where does in_len comes from? - // in_len.min(write_tracker.remain_data_len as usize); + .min(write_tracker.remain_data_len as usize); let data_segment = // Fails!! source.dec_as_binstring(usable_data)?; @@ -546,7 +545,7 @@ where Ok(()) } - // TODO: Handle more long requests + // TODO Handle more long requests /// Handles long request that do not fit in the buffers and stores a tracker /// /// **WARNING:** Only `SSH_FXP_WRITE` has been implemented! @@ -593,7 +592,6 @@ where "Storing a write tracker for a fragmented write request" ); return Ok(write_tracker); - // partial_write_request_tracker = Some(write_tracker); // TODO: This might belong to return value } Err(e) => { error!("SFTP write thrown: {:?}", e); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 22f5bffb..38441404 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,5 +1,5 @@ use crate::{ - OpaqueFileHandle, + handles::OpaqueFileHandle, proto::{Attrs, Name, StatusCode}, }; @@ -88,7 +88,7 @@ where } } -// TODO: Define this +// TODO Define this pub struct ReadReply<'g, 'a> { chan: ChanOut<'g, 'a>, } @@ -97,7 +97,7 @@ impl<'g, 'a> ReadReply<'g, 'a> { pub fn reply(self, _data: &[u8]) {} } -// TODO: Define this +// TODO Define this pub struct DirReply<'g, 'a> { chan: ChanOut<'g, 'a>, } @@ -106,7 +106,7 @@ impl<'g, 'a> DirReply<'g, 'a> { pub fn reply(self, _data: &[u8]) {} } -// TODO: Implement correct Channel Out +// TODO Implement correct Channel Out pub struct ChanOut<'g, 'a> { _phantom_g: PhantomData<&'g ()>, _phantom_a: PhantomData<&'a ()>, diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index b1f0a2c0..67b743e8 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -1,9 +1,11 @@ +use crate::error::{SftpError, SftpResult}; +use crate::handles::OpaqueFileHandle; use crate::proto::{ ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, SFTP_MINIMUM_PACKET_LEN, SFTP_WRITE_REQID_INDEX, SftpNum, }; +use crate::protocol::FileHandle; use crate::sftphandler::PartialWriteRequestTracker; -use crate::{FileHandle, OpaqueFileHandle, SftpError, SftpResult}; use sunset::sshwire::{BinString, SSHDecode, SSHSource, WireError, WireResult}; From fe25c4a010dbe1e6f9f7a238c039e2bbd3ed481e Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 3 Oct 2025 17:43:05 +1000 Subject: [PATCH 312/393] Partial functionality and refactor completed In this commit we have a working server that can receive files of arbitrary size. This can be tested running demo/sftp/std/ and running `demo/sftp/std/testing/test_long_write_requests.sh` Significative documentation has been added. SftpHandler relies on a FSM to process fragmented and long packet. --- Cargo.lock | 2 +- sftp/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17e2beb8..17936b90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2915,7 +2915,7 @@ dependencies = [ [[package]] name = "sunset-sftp" -version = "0.1.0" +version = "0.1.1" dependencies = [ "log", "num_enum 0.7.4", diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 2deb566d..3d500f3b 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sunset-sftp" -version = "0.1.0" +version = "0.1.1" edition = "2024" [dependencies] From 47e03e7304ce1c2880ad65a35ebba188a480b3ad Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 6 Oct 2025 11:58:16 +1100 Subject: [PATCH 313/393] SFTP Roadmap added to docs The crate doc has been rendered and improved. I propose a roadmap for the development of teh SFTP crate --- demo/sftp/std/src/demosftpserver.rs | 1 + sftp/src/lib.rs | 57 ++++++++++++++++++++++++++--- sftp/src/sftperror.rs | 3 +- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 8ad893c5..8e763185 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -29,6 +29,7 @@ impl PathFinder for PrivateFileHandler { } } +/// A basic demo server. Used as a demo and to test SFTP functionality pub struct DemoSftpServer { base_path: String, handlers_manager: diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 2fc6465b..a319faaa 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -1,12 +1,51 @@ -//! SFTP (SSH File Transfer Protocol) implementation extending [`sunset`]. +//! SFTP (SSH File Transfer Protocol) implementation for [`sunset`]. //! -//! Partially Implements SFTP v3 as defined in [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02). +//! (Partially) Implements SFTP v3 as defined in [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02). //! //! **Work in Progress**: Currently focuses on file upload operations. -//! Long packets for other request types and additional SFTP operations are not yet implemented. -//! `no_std` compatibility is intended but not yet complete. +//! Long packets for requests other than writing and additional SFTP operations +//! are not yet implemented. `no_std` compatibility is intended but not +//! yet complete. Please see the roadmap and use this crate carefully. //! -//! See example usage in the `demo/sftd/std` directory. +//! This crate implements a handler that, given a [`sunset::ChanHandle`] +//! a `sunset_async::SSHServer` and some auxiliary buffers, +//! can dispatch SFTP packets to a struct implementing [`crate::sftpserver::SftpServer`] trait. +//! +//! See example usage in the `../demo/sftd/std` directory for the intended usage +//! of this library. +//! +//! # Roadmap +//! +//! The following list is an opinionated collection of the points that should be +//! completed to provide growing functionality. +//! +//! ## Basic features +//! +//! - [x] [SFTP Protocol Initialization](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-4) (Only SFTP V3 supported) +//! - [x] [Canonicalizing the Server-Side Path Name](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.11) support +//! - [x] [Open, close](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3) +//! and [write](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) +//! - [ ] File [read](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4), +//! - [ ] File [stats](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8) +//! - [ ] Directory [Browsing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7) +//! +//! ## Minimal features for convenient usability +//! +//! - [ ] [Removing files](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.5) +//! - [ ] [Renaming files](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.5) +//! - [ ] [Creating directories](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.6) +//! - [ ] [Removing directories](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.6) +//! +//! ## Extended features +//! +//! - [ ] [Append, create and truncate files](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3) +//! files +//! - [ ] [Reading](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8) +//! files attributes +//! - [ ] [Setting](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.9) files attributes +//! - [ ] [Dealing with Symbolic links](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.10) +//! - [ ] [Vendor Specific](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-8) +//! request and responses #![forbid(unsafe_code)] #![warn(missing_docs)] @@ -20,8 +59,13 @@ mod sftpserver; mod sftpsink; mod sftpsource; +/// Main calling point for the library provided that the user implements +/// a [`crate::sftpserver::SftpServer`]. +/// +/// Please see basic usage at `../demo/sftd/std` pub use sftphandler::SftpHandler; +/// Structures and types used to add the details for the target system pub mod server { pub use crate::sftpserver::DirReply; @@ -30,12 +74,14 @@ pub mod server { pub use crate::sftpserver::SftpServer; } +/// Handles and helpers used by the [`sftpserver::SftpServer`] trait implementer pub mod handles { pub use crate::opaquefilehandle::OpaqueFileHandle; pub use crate::opaquefilehandle::OpaqueFileHandleManager; pub use crate::opaquefilehandle::PathFinder; } +/// SFTP Protocol types and structures pub mod protocol { pub use crate::proto::Attrs; pub use crate::proto::FileHandle; @@ -46,6 +92,7 @@ pub mod protocol { pub use crate::proto::StatusCode; } +/// Errors and results used in this crate pub mod error { pub use crate::sftperror::SftpError; pub use crate::sftperror::SftpResult; diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index 134e716c..553d622e 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -27,7 +27,8 @@ pub enum SftpError { NotSupported, /// The [`crate::sftpserver::SftpServer`] failed doing an IO operation FileServerError(StatusCode), - /// A RequestHolder instance throw an error. See [`crate::requestholder::RequestHolderError`] + // A RequestHolder instance throw an error. See [`crate::requestholder::RequestHolderError`] + /// A RequestHolder instance threw an error. See `RequestHolderError` RequestHolderError(RequestHolderError), /// A variant containing a [`WireError`] WireError(WireError), From 5becfb1850697b51769323c1f094df2b4d8789d7 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 8 Oct 2025 17:38:37 +1100 Subject: [PATCH 314/393] [ci skip] Adding no_std, removing unnecessary use of String --- sftp/src/lib.rs | 1 + sftp/src/sftpserver.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index a319faaa..de45fec9 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -49,6 +49,7 @@ #![forbid(unsafe_code)] #![warn(missing_docs)] +#![no_std] mod opaquefilehandle; mod proto; diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 38441404..e66b3ac7 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -54,10 +54,10 @@ where buf: &[u8], ) -> SftpOpResult<()> { log::error!( - "SftpServer Write operation: handle = {:?}, offset = {:?}, buf = {:?}", + "SftpServer Write operation not defined: handle = {:?}, offset = {:?}, buf = {:?}", opaque_file_handle, offset, - String::from_utf8(buf.to_vec()) + buf ); Ok(()) } From 1320d34d453019464a3b24e1fd4b2e57375f5451 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 9 Oct 2025 13:53:16 +1100 Subject: [PATCH 315/393] [ci skip] WIP: Added OpenDir operation SftpServer trait and DemoSftpServer modified to allow the use of Directories --- demo/sftp/std/src/demosftpserver.rs | 227 ++++++++++++++++++++-------- sftp/src/proto.rs | 14 +- sftp/src/sftpserver.rs | 4 +- 3 files changed, 178 insertions(+), 67 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 8e763185..9d4044c0 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -9,35 +9,87 @@ use sunset_sftp::server::{DirReply, ReadReply, SftpOpResult, SftpServer}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -use std::{fs::File, os::unix::fs::FileExt}; +use std::fs; +use std::{fs::File, os::unix::fs::FileExt, path::Path}; -pub(crate) struct PrivateFileHandler { - file_path: String, +pub(crate) enum PrivatePathHandle { + File(PrivateFileHandle), + Directory(PrivateDirHandle), +} + +pub(crate) struct PrivateFileHandle { + path: String, permissions: Option, file: File, } +pub(crate) struct PrivateDirHandle { + path: String, +} + static OPAQUE_SALT: &'static str = "12d%32"; -impl PathFinder for PrivateFileHandler { - fn matches(&self, path: &PrivateFileHandler) -> bool { - self.file_path.as_str().eq_ignore_ascii_case(path.get_path_ref()) +impl PathFinder for PrivatePathHandle { + fn matches(&self, path: &Self) -> bool { + match self { + PrivatePathHandle::File(self_private_path_handler) => { + if let PrivatePathHandle::File(private_file_handle) = path { + return self_private_path_handler.matches(private_file_handle); + } else { + false + } + } + PrivatePathHandle::Directory(self_private_dir_handle) => { + if let PrivatePathHandle::Directory(private_dir_handle) = path { + self_private_dir_handle.matches(private_dir_handle) + } else { + false + } + } + } } fn get_path_ref(&self) -> &str { - self.file_path.as_str() + match self { + PrivatePathHandle::File(private_file_handler) => { + private_file_handler.get_path_ref() + } + PrivatePathHandle::Directory(private_dir_handle) => { + private_dir_handle.get_path_ref() + } + } + } +} + +impl PathFinder for PrivateFileHandle { + fn matches(&self, path: &PrivateFileHandle) -> bool { + self.path.as_str().eq_ignore_ascii_case(path.get_path_ref()) + } + + fn get_path_ref(&self) -> &str { + self.path.as_str() + } +} + +impl PathFinder for PrivateDirHandle { + fn matches(&self, path: &PrivateDirHandle) -> bool { + self.path.as_str().eq_ignore_ascii_case(path.get_path_ref()) + } + + fn get_path_ref(&self) -> &str { + self.path.as_str() } } /// A basic demo server. Used as a demo and to test SFTP functionality pub struct DemoSftpServer { base_path: String, - handlers_manager: - DemoFileHandleManager, + handlers_manager: DemoFileHandleManager, } impl DemoSftpServer { pub fn new(base_path: String) -> Self { + // TODO What if the base_path does not exist? Create it or Return error? DemoSftpServer { base_path, handlers_manager: DemoFileHandleManager::new() } } } @@ -48,41 +100,71 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { filename: &str, attrs: &Attrs, ) -> SftpOpResult { - debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); - - let poxit_attr = attrs - .permissions - .as_ref() - .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED)?; - let can_write = poxit_attr & 0o222 > 0; - let can_read = poxit_attr & 0o444 > 0; - debug!( - "File open for read/write access: can_read={:?}, can_write={:?}", - can_read, can_write - ); + let path = Path::new(filename); - let file = File::options() - .read(can_read) - .write(can_write) - .create(true) - .open(filename) - .map_err(|_| StatusCode::SSH_FX_FAILURE)?; - - let fh = self.handlers_manager.insert( - PrivateFileHandler { - file_path: filename.into(), - permissions: attrs.permissions, - file, - }, - OPAQUE_SALT, - ); + let metadata = fs::symlink_metadata(path).map_err(|e| { + warn!("Could not open {:?}: {:?}", filename, e); + StatusCode::SSH_FX_NO_SUCH_FILE + })?; - debug!( - "Filename \"{:?}\" will have the obscured file handle: {:?}", - filename, fh - ); + if metadata.is_symlink() { + return Err(StatusCode::SSH_FX_OP_UNSUPPORTED); + } + + if metadata.is_dir() { + debug!("Open Directory = {:?}", filename); - fh + let fh = self.handlers_manager.insert( + PrivatePathHandle::Directory(PrivateDirHandle { + path: filename.into(), + }), + OPAQUE_SALT, + ); + + debug!( + "Directory \"{:?}\" will have the obscured file handle: {:?}", + filename, fh + ); + + // Err(StatusCode::SSH_FX_BAD_MESSAGE) + fh + } else { + debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); + + let poxit_attr = attrs + .permissions + .as_ref() + .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED)?; + let can_write = poxit_attr & 0o222 > 0; + let can_read = poxit_attr & 0o444 > 0; + debug!( + "File open for read/write access: can_read={:?}, can_write={:?}", + can_read, can_write + ); + + let file = File::options() + .read(can_read) + .write(can_write) + .create(true) + .open(filename) + .map_err(|_| StatusCode::SSH_FX_FAILURE)?; + + let fh = self.handlers_manager.insert( + PrivatePathHandle::File(PrivateFileHandle { + path: filename.into(), + permissions: attrs.permissions, + file, + }), + OPAQUE_SALT, + ); + + debug!( + "Filename \"{:?}\" will have the obscured file handle: {:?}", + filename, fh + ); + + fh + } } fn realpath(&mut self, dir: &str) -> SftpOpResult> { @@ -107,12 +189,24 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { opaque_file_handle: &DemoOpaqueFileHandle, ) -> SftpOpResult<()> { if let Some(handle) = self.handlers_manager.remove(opaque_file_handle) { - debug!( - "SftpServer Close operation on {:?} was successful", - handle.file_path - ); - drop(handle.file); // Not really required but illustrative - Ok(()) + match handle { + PrivatePathHandle::File(private_file_handle) => { + debug!( + "SftpServer Close operation on file {:?} was successful", + private_file_handle.path + ); + drop(private_file_handle.file); // Not really required but illustrative + Ok(()) + } + PrivatePathHandle::Directory(private_dir_handle) => { + debug!( + "SftpServer Close operation on dir {:?} was successful", + private_dir_handle.path + ); + + Ok(()) + } + } } else { error!( "SftpServer Close operation on handle {:?} failed", @@ -128,41 +222,44 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { offset: u64, buf: &[u8], ) -> SftpOpResult<()> { - let private_file_handle = self + if let PrivatePathHandle::File(private_file_handle) = self .handlers_manager .get_private_as_ref(opaque_file_handle) - .ok_or(StatusCode::SSH_FX_FAILURE)?; - - let permissions_poxit = (private_file_handle - .permissions - .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED))?; + .ok_or(StatusCode::SSH_FX_FAILURE)? + { + let permissions_poxit = (private_file_handle + .permissions + .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED))?; - if (permissions_poxit & 0o222) == 0 { - return Err(StatusCode::SSH_FX_PERMISSION_DENIED); - }; + if (permissions_poxit & 0o222) == 0 { + return Err(StatusCode::SSH_FX_PERMISSION_DENIED); + }; - log::trace!( + log::trace!( "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buf = {:?}", opaque_file_handle, - private_file_handle.file_path, + private_file_handle.path, offset, String::from_utf8(buf.to_vec()) ); - let bytes_written = private_file_handle - .file - .write_at(buf, offset) - .map_err(|_| StatusCode::SSH_FX_FAILURE)?; + let bytes_written = private_file_handle + .file + .write_at(buf, offset) + .map_err(|_| StatusCode::SSH_FX_FAILURE)?; - log::debug!( + log::debug!( "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buffer length = {:?}, bytes written = {:?}", opaque_file_handle, - private_file_handle.file_path, + private_file_handle.path, offset, buf.len(), bytes_written ); - Ok(()) + Ok(()) + } else { + Err(StatusCode::SSH_FX_PERMISSION_DENIED) + } } fn read( diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index cd606478..1ded97ad 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -60,6 +60,8 @@ impl<'a> Filename<'a> { #[derive(Debug, Clone, Copy, PartialEq, Eq, SSHEncode, SSHDecode)] pub struct FileHandle<'a>(pub BinString<'a>); +// ========================== Initialization =========================== + /// The reference implementation we are working on is 3, this is, https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 pub const SFTP_VERSION: u32 = 3; @@ -79,6 +81,8 @@ pub struct InitVersionLowest { // TODO variable number of ExtPair } +// ============================= Requests ============================== + /// Used for `ssh_fxp_open` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Open<'a> { @@ -90,6 +94,13 @@ pub struct Open<'a> { pub attrs: Attrs, } +/// Used for `ssh_fxp_open` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3). +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct OpenDir<'a> { + /// The relative or absolute path of the directory to be open + pub filename: Filename<'a>, +} + /// Used for `ssh_fxp_close` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Close<'a> { @@ -122,7 +133,7 @@ pub struct Write<'a> { pub data: BinString<'a>, } -// Responses +// ============================= Responses ============================= /// Used for `ssh_fxp_realpath` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.11). #[derive(Debug, SSHEncode, SSHDecode)] @@ -757,6 +768,7 @@ sftpmessages! [ (4, Close, Close<'a>, "ssh_fxp_close"), (5, Read, Read<'a>, "ssh_fxp_read"), (6, Write, Write<'a>, "ssh_fxp_write"), + (11, OpenDir, OpenDir<'a>, "ssh_fxp_opendir"), (16, PathInfo, PathInfo<'a>, "ssh_fxp_realpath"), }, diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index e66b3ac7..a664a280 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -17,7 +17,9 @@ where T: OpaqueFileHandle, { /// Opens a file or directory for reading/writing - fn open(&'_ mut self, filename: &str, attrs: &Attrs) -> SftpOpResult { + /// + /// In the case of a path the attrs parameter content is ignored + fn open(&'_ mut self, path: &str, attrs: &Attrs) -> SftpOpResult { log::error!( "SftpServer Open operation not defined: filename = {:?}, attrs = {:?}", filename, From 2c9bf39b14f6c47c5972c131c899feb559bf5452 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 9 Oct 2025 16:41:13 +1100 Subject: [PATCH 316/393] [skip ci] WIP: Added OpenDir and ReadDir requests I am at the point where I can start implementing the way I am going to put all the data in the channel. Not trivial --- demo/sftp/std/src/demosftpserver.rs | 246 +++++++++++++++---------- demo/sftp/std/testing/test_read_dir.sh | 30 +++ sftp/Cargo.toml | 3 + sftp/src/proto.rs | 13 +- sftp/src/sftperror.rs | 3 + sftp/src/sftphandler.rs | 44 ++++- sftp/src/sftpserver.rs | 16 +- 7 files changed, 240 insertions(+), 115 deletions(-) create mode 100755 demo/sftp/std/testing/test_read_dir.sh diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 9d4044c0..33272ef5 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -5,24 +5,30 @@ use crate::{ use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; -use sunset_sftp::server::{DirReply, ReadReply, SftpOpResult, SftpServer}; +use sunset_sftp::server::{ReadReply, SftpOpResult, SftpServer}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; use std::fs; +use std::os::linux::fs::MetadataExt; +use std::os::unix::fs::PermissionsExt; +use std::time::SystemTime; use std::{fs::File, os::unix::fs::FileExt, path::Path}; +#[derive(Debug)] pub(crate) enum PrivatePathHandle { File(PrivateFileHandle), Directory(PrivateDirHandle), } +#[derive(Debug)] pub(crate) struct PrivateFileHandle { path: String, permissions: Option, file: File, } +#[derive(Debug)] pub(crate) struct PrivateDirHandle { path: String, } @@ -84,13 +90,13 @@ impl PathFinder for PrivateDirHandle { /// A basic demo server. Used as a demo and to test SFTP functionality pub struct DemoSftpServer { base_path: String, - handlers_manager: DemoFileHandleManager, + handles_manager: DemoFileHandleManager, } impl DemoSftpServer { pub fn new(base_path: String) -> Self { // TODO What if the base_path does not exist? Create it or Return error? - DemoSftpServer { base_path, handlers_manager: DemoFileHandleManager::new() } + DemoSftpServer { base_path, handles_manager: DemoFileHandleManager::new() } } } @@ -100,71 +106,57 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { filename: &str, attrs: &Attrs, ) -> SftpOpResult { - let path = Path::new(filename); - - let metadata = fs::symlink_metadata(path).map_err(|e| { - warn!("Could not open {:?}: {:?}", filename, e); - StatusCode::SSH_FX_NO_SUCH_FILE - })?; - - if metadata.is_symlink() { - return Err(StatusCode::SSH_FX_OP_UNSUPPORTED); - } - - if metadata.is_dir() { - debug!("Open Directory = {:?}", filename); - - let fh = self.handlers_manager.insert( - PrivatePathHandle::Directory(PrivateDirHandle { - path: filename.into(), - }), - OPAQUE_SALT, - ); + debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); + + let poxit_attr = attrs + .permissions + .as_ref() + .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED)?; + let can_write = poxit_attr & 0o222 > 0; + let can_read = poxit_attr & 0o444 > 0; + debug!( + "File open for read/write access: can_read={:?}, can_write={:?}", + can_read, can_write + ); - debug!( - "Directory \"{:?}\" will have the obscured file handle: {:?}", - filename, fh - ); + let file = File::options() + .read(can_read) + .write(can_write) + .create(true) + .open(filename) + .map_err(|_| StatusCode::SSH_FX_FAILURE)?; + + let fh = self.handles_manager.insert( + PrivatePathHandle::File(PrivateFileHandle { + path: filename.into(), + permissions: attrs.permissions, + file, + }), + OPAQUE_SALT, + ); - // Err(StatusCode::SSH_FX_BAD_MESSAGE) - fh - } else { - debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); + debug!( + "Filename \"{:?}\" will have the obscured file handle: {:?}", + filename, fh + ); - let poxit_attr = attrs - .permissions - .as_ref() - .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED)?; - let can_write = poxit_attr & 0o222 > 0; - let can_read = poxit_attr & 0o444 > 0; - debug!( - "File open for read/write access: can_read={:?}, can_write={:?}", - can_read, can_write - ); + fh + } - let file = File::options() - .read(can_read) - .write(can_write) - .create(true) - .open(filename) - .map_err(|_| StatusCode::SSH_FX_FAILURE)?; + fn opendir(&mut self, dir: &str) -> SftpOpResult { + debug!("Open Directory = {:?}", dir); - let fh = self.handlers_manager.insert( - PrivatePathHandle::File(PrivateFileHandle { - path: filename.into(), - permissions: attrs.permissions, - file, - }), - OPAQUE_SALT, - ); + let dir_handle = self.handles_manager.insert( + PrivatePathHandle::Directory(PrivateDirHandle { path: dir.into() }), + OPAQUE_SALT, + ); - debug!( - "Filename \"{:?}\" will have the obscured file handle: {:?}", - filename, fh - ); + debug!( + "Directory \"{:?}\" will have the obscured file handle: {:?}", + dir, dir_handle + ); - fh - } + dir_handle } fn realpath(&mut self, dir: &str) -> SftpOpResult> { @@ -188,7 +180,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { &mut self, opaque_file_handle: &DemoOpaqueFileHandle, ) -> SftpOpResult<()> { - if let Some(handle) = self.handlers_manager.remove(opaque_file_handle) { + if let Some(handle) = self.handles_manager.remove(opaque_file_handle) { match handle { PrivatePathHandle::File(private_file_handle) => { debug!( @@ -216,6 +208,20 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } } + fn read( + &mut self, + opaque_file_handle: &DemoOpaqueFileHandle, + offset: u64, + _reply: &mut ReadReply<'_, '_>, + ) -> SftpOpResult<()> { + log::error!( + "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", + opaque_file_handle, + offset + ); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + fn write( &mut self, opaque_file_handle: &DemoOpaqueFileHandle, @@ -223,7 +229,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { buf: &[u8], ) -> SftpOpResult<()> { if let PrivatePathHandle::File(private_file_handle) = self - .handlers_manager + .handles_manager .get_private_as_ref(opaque_file_handle) .ok_or(StatusCode::SSH_FX_FAILURE)? { @@ -236,12 +242,12 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { }; log::trace!( - "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buf = {:?}", - opaque_file_handle, - private_file_handle.path, - offset, - String::from_utf8(buf.to_vec()) - ); + "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buf = {:?}", + opaque_file_handle, + private_file_handle.path, + offset, + String::from_utf8(buf.to_vec()) + ); let bytes_written = private_file_handle .file .write_at(buf, offset) @@ -249,12 +255,12 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { log::debug!( "SftpServer Write operation: handle = {:?}, filepath = {:?}, offset = {:?}, buffer length = {:?}, bytes written = {:?}", - opaque_file_handle, - private_file_handle.path, - offset, - buf.len(), - bytes_written - ); + opaque_file_handle, + private_file_handle.path, + offset, + buf.len(), + bytes_written + ); Ok(()) } else { @@ -262,34 +268,74 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } } - fn read( - &mut self, - opaque_file_handle: &DemoOpaqueFileHandle, - offset: u64, - _reply: &mut ReadReply<'_, '_>, - ) -> SftpOpResult<()> { - log::error!( - "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", - opaque_file_handle, - offset - ); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) - } - - fn opendir(&mut self, dir: &str) -> SftpOpResult { - log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) - } - fn readdir( &mut self, - opaque_file_handle: &DemoOpaqueFileHandle, - _reply: &mut DirReply<'_, '_>, + opaque_dir_handle: &DemoOpaqueFileHandle, + // _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { - log::error!( - "SftpServer ReadDir operation not defined: handle = {:?}", - opaque_file_handle - ); + debug!("read dir for {:?}", opaque_dir_handle); + if let PrivatePathHandle::Directory(dir) = self + .handles_manager + .get_private_as_ref(opaque_dir_handle) + .ok_or(StatusCode::SSH_FX_FAILURE)? + { + let path_str = dir.path.clone(); + debug!("opaque handle found in handles manager: {:?}", path_str); + let dir_path = Path::new(&path_str); + debug!("path: {:?}", dir_path); + if dir_path.is_dir() { + debug!("SftpServer ReadDir operation path = {:?}", dir_path); + + let dir_iterator = fs::read_dir(dir_path).map_err(|err| { + error!("could not get the directory {:?}: {:?}", path_str, err); + StatusCode::SSH_FX_PERMISSION_DENIED + })?; + + debug!("got iterator = {:?}", dir_iterator); + + for entry in dir_iterator { + if let Ok(entry) = entry { + // info!("{:?}", entry); + if let Ok(metadata) = entry.metadata() { + if metadata.accessed().is_ok() + && metadata.modified().is_ok() + { + info!( + "{:?} : size = {:?}, uid = {:?}, gid = {:?}, \ + permissions = {:?}, atime = {:?}, mtime = {:?}, \ + ", + entry.path(), + metadata.len(), + metadata.st_uid(), + metadata.st_uid(), + metadata.permissions().mode(), + metadata + .accessed() + .unwrap() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(), + metadata + .modified() + .unwrap() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(), + ); + } + } + } + } + } else { + error!("the path is not a directory = {:?}", dir_path); + return Err(StatusCode::SSH_FX_NO_SUCH_FILE); + } + } else { + error!("Could not find the directory for {:?}", opaque_dir_handle); + return Err(StatusCode::SSH_FX_NO_SUCH_FILE); + } + + error!("What is the return that we are looking for?"); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } } diff --git a/demo/sftp/std/testing/test_read_dir.sh b/demo/sftp/std/testing/test_read_dir.sh new file mode 100755 index 00000000..75d0dcb3 --- /dev/null +++ b/demo/sftp/std/testing/test_read_dir.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Define test files +FILES=("A_random" "B_random" "D_random" "E_random" "F_random" "G_random") + +# Generate random data files +echo "Generating random data files..." +dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null + +# Generating copies of the test file +echo "Creating copies for each test file..." +for file in "${FILES[@]}"; do + cp ./512B_random "./${file}" + echo "Created: ${file}" +done +ls + +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." + +# Upload all files +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +$(printf 'put ./%s\n' "${FILES[@]}") +ls +bye +EOF + diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 3d500f3b..9a591f9f 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -5,8 +5,11 @@ edition = "2024" [dependencies] sunset = { version = "0.3.0", path = "../" } +sunset-async = { path = "../async", version = "0.3" } sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } + +embedded-io-async = "0.6" num_enum = {version = "0.7.4"} paste = "1.0" log = "0.4" diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 1ded97ad..de466721 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -94,11 +94,11 @@ pub struct Open<'a> { pub attrs: Attrs, } -/// Used for `ssh_fxp_open` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3). +/// Used for `ssh_fxp_open` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7). #[derive(Debug, SSHEncode, SSHDecode)] pub struct OpenDir<'a> { /// The relative or absolute path of the directory to be open - pub filename: Filename<'a>, + pub dirname: Filename<'a>, } /// Used for `ssh_fxp_close` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3). @@ -121,6 +121,14 @@ pub struct Read<'a> { pub len: u32, } +/// Used for `ssh_fxp_readdir` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7). +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct ReadDir<'a> { + /// An opaque handle that is used by the server to identify an open + /// file or folder. + pub handle: FileHandle<'a>, +} + /// Used for `ssh_fxp_write` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Write<'a> { @@ -769,6 +777,7 @@ sftpmessages! [ (5, Read, Read<'a>, "ssh_fxp_read"), (6, Write, Write<'a>, "ssh_fxp_write"), (11, OpenDir, OpenDir<'a>, "ssh_fxp_opendir"), + (12, ReadDir, ReadDir<'a>, "ssh_fxp_readdir"), (16, PathInfo, PathInfo<'a>, "ssh_fxp_realpath"), }, diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index 553d622e..cd01f748 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -25,6 +25,8 @@ pub enum SftpError { /// - The request has not been handled by an [`crate::sftpserver::SftpServer`] /// - Long request which its handling was not implemented NotSupported, + /// The connection has been closed by the client + ClientDisconnected, /// The [`crate::sftpserver::SftpServer`] failed doing an IO operation FileServerError(StatusCode), // A RequestHolder instance throw an error. See [`crate::requestholder::RequestHolderError`] @@ -98,6 +100,7 @@ impl From for SunsetError { warn!("Casting error loosing information: {:?}", value); SunsetError::Bug } + SftpError::ClientDisconnected => SunsetError::ChannelEOF, } } } diff --git a/sftp/src/sftphandler.rs b/sftp/src/sftphandler.rs index a839919a..ecd6981c 100644 --- a/sftp/src/sftphandler.rs +++ b/sftp/src/sftphandler.rs @@ -12,8 +12,10 @@ use crate::sftpsource::SftpSource; use sunset::Error as SunsetError; use sunset::sshwire::{SSHSource, WireError, WireResult}; +use sunset_async::ChanInOut; -use core::{u32, usize}; +use core::u32; +use embedded_io_async::{Read, Write}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; @@ -135,7 +137,6 @@ where pub async fn process( &mut self, buffer_in: &[u8], - // incomplete_request_holder: &mut RequestHolder<'_>, buffer_out: &mut [u8], ) -> SftpResult { let in_len = buffer_in.len(); @@ -537,9 +538,44 @@ where } } } + SftpPacket::OpenDir(req_id, open_dir) => { + match file_server.opendir(open_dir.dirname.as_str()?) { + Ok(opaque_file_handle) => { + let response = SftpPacket::Handle( + req_id, + proto::Handle { + handle: opaque_file_handle.into_file_handle(), + }, + ); + response.encode_response(sink)?; + info!("Sending '{:?}'", response); + } + Err(status_code) => { + error!("Open failed: {:?}", status_code); + push_general_failure(req_id, "", sink)?; + } + }; + } + SftpPacket::ReadDir(req_id, read_dir) => { + // TODO Implement the mechanism you are going to use to + // handle the list of elements + match file_server.readdir(&T::try_from(&read_dir.handle)?) { + Ok(_) => { + todo!("Dance starts here"); + } + Err(status_code) => { + error!("Open failed: {:?}", status_code); + push_unsupported(req_id, sink)?; + } + }; + error!("Unsupported Read Dir : {:?}", read_dir); + // return Err(SftpError::NotSupported); + // push_unsupported(ReqId(0), sink)?; + } _ => { - error!("Unsuported request type"); - push_unsupported(ReqId(0), sink)?; + error!("Unsuported request type: {:?}", request); + return Err(SftpError::NotSupported); + // push_unsupported(ReqId(0), sink)?; } } Ok(()) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index a664a280..05cdfc22 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -16,13 +16,11 @@ pub trait SftpServer<'a, T> where T: OpaqueFileHandle, { - /// Opens a file or directory for reading/writing - /// - /// In the case of a path the attrs parameter content is ignored + /// Opens a file for reading/writing fn open(&'_ mut self, path: &str, attrs: &Attrs) -> SftpOpResult { log::error!( - "SftpServer Open operation not defined: filename = {:?}, attrs = {:?}", - filename, + "SftpServer Open operation not defined: path = {:?}, attrs = {:?}", + path, attrs ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -64,7 +62,7 @@ where Ok(()) } - /// Opens a directory + /// Opens a directory and returns a handle fn opendir(&mut self, dir: &str) -> SftpOpResult { log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -73,12 +71,12 @@ where /// Reads the list of items in a directory fn readdir( &mut self, - opaque_file_handle: &T, - _reply: &mut DirReply<'_, '_>, + opaque_dir_handle: &T, + // _reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", - opaque_file_handle + opaque_dir_handle ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } From 823ba9ba3e8a7aa31e3018435aad2e13fd21e611 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 10 Oct 2025 16:05:06 +1100 Subject: [PATCH 317/393] [skip ci] WIP: moving main loop inside process_loop to have stdio in scope --- demo/sftp/std/src/main.rs | 18 +++--------------- sftp/src/sftphandler.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 51d4f24f..74fccb3a 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -164,22 +164,10 @@ impl DemoServer for StdDemo { &mut file_server, &mut incomplete_request_buffer, ); - loop { - let lr = stdio.read(&mut buffer_in).await?; - trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); - if lr == 0 { - debug!("client disconnected"); - break; - } + sftp_handler + .process_loop(&mut stdio, &mut buffer_in, &mut buffer_out) + .await?; - let lw = sftp_handler - .process(&buffer_in[0..lr], &mut buffer_out) - .await?; - if lw > 0 { - stdio.write(&mut buffer_out[0..lw]).await?; - trace!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); - } - } Ok::<_, Error>(()) } { Ok(_) => { diff --git a/sftp/src/sftphandler.rs b/sftp/src/sftphandler.rs index ecd6981c..e3bd9c82 100644 --- a/sftp/src/sftphandler.rs +++ b/sftp/src/sftphandler.rs @@ -478,6 +478,38 @@ where Ok(used_out_accumulated_index) } + /// WIP: A loop that will process all the request from stdio until + /// an EOF is received + pub async fn process_loop<'c>( + &mut self, + stdio: &mut ChanInOut<'c>, + buffer_in: &mut [u8], + buffer_out: &mut [u8], + ) -> SftpResult<()> { + loop { + let lr = stdio.read(buffer_in).await?; + trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); + if lr == 0 { + debug!("client disconnected"); + return Err(SftpError::ClientDisconnected); + } + + let lw = self.process(&buffer_in[0..lr], buffer_out).await?; + if lw > 0 { + let wo = stdio.write(&mut buffer_out[0..lw]).await?; + if wo != lw { + error!("SFTP ----> Sent incomplete {} < {}", wo, lw); + todo!( + "Fix write incomplete condition: Repeat until \ + all is gone down the channel" + ); + } + trace!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); + } + } + Ok(()) + } + fn handle_general_request( file_server: &mut S, sink: &mut SftpSink<'_>, From a7aa58cbc59251f2de3fe48ecb2e6a9232a0d6ba Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 10 Oct 2025 16:05:42 +1100 Subject: [PATCH 318/393] [skip ci] WIP: commenting out the no_std restriction for now --- sftp/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index de45fec9..740072c7 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -49,7 +49,7 @@ #![forbid(unsafe_code)] #![warn(missing_docs)] -#![no_std] +// #![no_std] mod opaquefilehandle; mod proto; From 2b5d934b90c4dffd31f6d89c5907ae2daf4d3bb3 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 10 Oct 2025 16:06:51 +1100 Subject: [PATCH 319/393] [skip ci] WIP: I missed out adding items to Cargo.lock --- Cargo.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 17936b90..de7fac7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2917,10 +2917,12 @@ dependencies = [ name = "sunset-sftp" version = "0.1.1" dependencies = [ + "embedded-io-async", "log", "num_enum 0.7.4", "paste", "sunset", + "sunset-async", "sunset-sshwire-derive", ] From 5b8e30c938b5e312e9a8212ffff2492a87c6af0a Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 10 Oct 2025 16:36:51 +1100 Subject: [PATCH 320/393] [skip ci] WIP: Exploring the iterator option [skip ci] WIP: Experimenting with SftpServer DirReply structure For now I am only checking borrowings [skip ci] WIP: SftpSink playload_slice() [skip ci] WIP: Adding DirReply Visitor to `ReadDir` and `DirEntriesResponseHelpers` to sftpserver.rs These will help me standardize other implementations. Also, I have proven that the Visitor pattern can be used to process each directory entry without borrowing or lifetime issues --- demo/sftp/std/src/demosftpserver.rs | 171 +++++++++++++++++++++------- sftp/src/lib.rs | 3 + sftp/src/sftphandler.rs | 12 +- sftp/src/sftpserver.rs | 70 +++++++++++- sftp/src/sftpsink.rs | 12 +- 5 files changed, 223 insertions(+), 45 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 33272ef5..e4688d2b 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -3,16 +3,21 @@ use crate::{ demoopaquefilehandle::DemoOpaqueFileHandle, }; +use sunset::sshwire::SSHEncode; use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; -use sunset_sftp::server::{ReadReply, SftpOpResult, SftpServer}; +use sunset_sftp::server::{ + DirEntriesResponseHelpers, DirReply, ReadReply, SftpOpResult, SftpServer, + SftpSink, +}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -use std::fs; +use std::fs::DirEntry; use std::os::linux::fs::MetadataExt; use std::os::unix::fs::PermissionsExt; use std::time::SystemTime; +use std::{fs, io}; use std::{fs::File, os::unix::fs::FileExt, path::Path}; #[derive(Debug)] @@ -271,9 +276,10 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { fn readdir( &mut self, opaque_dir_handle: &DemoOpaqueFileHandle, - // _reply: &mut DirReply<'_, '_>, + visitor: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { debug!("read dir for {:?}", opaque_dir_handle); + if let PrivatePathHandle::Directory(dir) = self .handles_manager .get_private_as_ref(opaque_dir_handle) @@ -283,6 +289,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { debug!("opaque handle found in handles manager: {:?}", path_str); let dir_path = Path::new(&path_str); debug!("path: {:?}", dir_path); + if dir_path.is_dir() { debug!("SftpServer ReadDir operation path = {:?}", dir_path); @@ -291,41 +298,17 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { StatusCode::SSH_FX_PERMISSION_DENIED })?; - debug!("got iterator = {:?}", dir_iterator); - - for entry in dir_iterator { - if let Ok(entry) = entry { - // info!("{:?}", entry); - if let Ok(metadata) = entry.metadata() { - if metadata.accessed().is_ok() - && metadata.modified().is_ok() - { - info!( - "{:?} : size = {:?}, uid = {:?}, gid = {:?}, \ - permissions = {:?}, atime = {:?}, mtime = {:?}, \ - ", - entry.path(), - metadata.len(), - metadata.st_uid(), - metadata.st_uid(), - metadata.permissions().mode(), - metadata - .accessed() - .unwrap() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(), - metadata - .modified() - .unwrap() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(), - ); - } - } - } - } + let name_entry_collection = DirEntriesCollection::new(dir_iterator); + + visitor.send_header( + name_entry_collection.get_count()?, + name_entry_collection.get_encoded_len()?, + ); + + name_entry_collection + .for_each_encoded(|data: &[u8]| visitor.send_item(data))?; + + // debug!("got iterator = {:?}", name_entry_collection); } else { error!("the path is not a directory = {:?}", dir_path); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); @@ -339,3 +322,115 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } } + +// TODO Add this to SFTP library only available with std as a global helper +#[derive(Debug)] +pub struct DirEntriesCollection { + /// Number of elements + count: u32, + /// Computed length of all the encoded elements + encoded_length: u32, + /// The actual entries. As you can see these are DirEntry. This is a std choice + entries: Vec, +} + +impl DirEntriesCollection { + pub fn new(dir_iterator: fs::ReadDir) -> Self { + let mut encoded_length = 0; + // This way I collect data required for the header and collect + // valid entries into a vector (only std) + let entries: Vec = dir_iterator + .filter_map(|entry_result| { + let entry = entry_result.ok()?; + + let filename = entry.path().to_string_lossy().into_owned(); + let name_entry = NameEntry { + filename: Filename::from(filename.as_str()), + _longname: Filename::from(""), + attrs: Self::get_attrs_or_empty(entry.metadata()), + }; + + const MAX_NAME_ENTRY_SIZE: usize = 4 + 256 + 4 + 72; // 4 + 256 bytes for path, 4 for empty long path and 72 bytes for the attributes ( 32/4*7 + 64/4 * 1 = 72) + + let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; + let mut sftp_sink = SftpSink::new(&mut buffer); + name_entry.enc(&mut sftp_sink).ok()?; + //TODO remove this unchecked casting + encoded_length += sftp_sink.payload_len() as u32; + Some(entry) + }) + .collect(); + + //TODO remove this unchecked casting + let count = entries.len() as u32; + + info!( + "Processed {} entries, estimated serialized length: {}", + count, encoded_length + ); + + Self { count, encoded_length, entries } + } + + fn get_attrs_or_empty( + maybe_metadata: Result, + ) -> Attrs { + maybe_metadata.map(Self::get_attrs).unwrap_or_default() + } + + fn get_attrs(metadata: fs::Metadata) -> Attrs { + let time_to_u32 = |time_result: io::Result| { + time_result + .ok()? + .duration_since(SystemTime::UNIX_EPOCH) + .ok()? + .as_secs() + .try_into() + .ok() + }; + + Attrs { + size: Some(metadata.len()), + uid: Some(metadata.st_uid()), + gid: Some(metadata.st_gid()), + permissions: Some(metadata.permissions().mode()), + atime: time_to_u32(metadata.accessed()), + mtime: time_to_u32(metadata.modified()), + ext_count: None, + } + } +} + +impl DirEntriesResponseHelpers for DirEntriesCollection { + fn get_count(&self) -> SftpOpResult { + Ok(self.count) + } + + fn get_encoded_len(&self) -> SftpOpResult { + Ok(self.encoded_length) + } + + fn for_each_encoded(&self, mut writer: F) -> SftpOpResult<()> + where + F: FnMut(&[u8]) -> (), + { + for entry in &self.entries { + let filename = entry.path().to_string_lossy().into_owned(); + let attrs = Self::get_attrs_or_empty(entry.metadata()); + let name_entry = NameEntry { + filename: Filename::from(filename.as_str()), + _longname: Filename::from(""), + attrs, + }; + debug!("Sending new item: {:?}", name_entry); + let mut buffer = [0u8; 4 + 256 + 4 + 72]; // 4 + 256 bytes for path, 4 for empty long path and 72 bytes for the attributes ( 32/4*7 + 64/4 * 1 = 72) + let mut sftp_sink = SftpSink::new(&mut buffer); + name_entry.enc(&mut sftp_sink).map_err(|err| { + debug!("WireError: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; + writer(sftp_sink.payload_slice()); + } + Ok(()) + } +} diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 740072c7..496bd475 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -69,10 +69,13 @@ pub use sftphandler::SftpHandler; /// Structures and types used to add the details for the target system pub mod server { + pub use crate::sftpserver::DirEntriesResponseHelpers; pub use crate::sftpserver::DirReply; pub use crate::sftpserver::ReadReply; pub use crate::sftpserver::SftpOpResult; pub use crate::sftpserver::SftpServer; + pub use crate::sftpsink::SftpSink; + pub use sunset::sshwire::SSHEncode; } /// Handles and helpers used by the [`sftpserver::SftpServer`] trait implementer diff --git a/sftp/src/sftphandler.rs b/sftp/src/sftphandler.rs index e3bd9c82..27148fde 100644 --- a/sftp/src/sftphandler.rs +++ b/sftp/src/sftphandler.rs @@ -5,6 +5,7 @@ use crate::proto::{ SftpPacket, Status, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; +use crate::server::DirReply; use crate::sftperror::SftpResult; use crate::sftpserver::SftpServer; use crate::sftpsink::SftpSink; @@ -487,6 +488,7 @@ where buffer_out: &mut [u8], ) -> SftpResult<()> { loop { + // let channelspub struct ChanOut<'g>(ChanIO<'g>) = stdio.split(); let lr = stdio.read(buffer_in).await?; trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); if lr == 0 { @@ -591,7 +593,14 @@ where SftpPacket::ReadDir(req_id, read_dir) => { // TODO Implement the mechanism you are going to use to // handle the list of elements - match file_server.readdir(&T::try_from(&read_dir.handle)?) { + + let mut muting = 0; + + let mut dir_reply = DirReply::mock(req_id, &mut muting); + + match file_server + .readdir(&T::try_from(&read_dir.handle)?, &mut dir_reply) + { Ok(_) => { todo!("Dance starts here"); } @@ -600,6 +609,7 @@ where push_unsupported(req_id, sink)?; } }; + debug!("final muting: {:?}", muting); error!("Unsupported Read Dir : {:?}", read_dir); // return Err(SftpError::NotSupported); // push_unsupported(ReqId(0), sink)?; diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 05cdfc22..70e7fe35 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,6 +1,8 @@ +use log::debug; + use crate::{ handles::OpaqueFileHandle, - proto::{Attrs, Name, StatusCode}, + proto::{Attrs, Name, ReqId, StatusCode}, }; use core::marker::PhantomData; @@ -72,7 +74,7 @@ where fn readdir( &mut self, opaque_dir_handle: &T, - // _reply: &mut DirReply<'_, '_>, + reply: &mut DirReply<'_, '_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", @@ -88,6 +90,41 @@ where } } +/// This trait is an standardized way to interact with an iterator or collection of Directory entries +/// that need to be sent via an SSH_FXP_READDIR SFTP response to a client. +/// +/// It uses is expected when implementing an [`SftpServer`] TODO Future trait WIP +pub trait DirEntriesResponseHelpers { + /// returns the number of directory entries. + /// Used for the `SSH_FXP_READDIR` response field `count` + /// as specified in [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) + fn get_count(&self) -> SftpOpResult { + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + /// Returns the total encoded length in bytes for all directory entries. + /// Used for the `SSH_FXP_READDIR` general response field `length` + /// as part of the [General Packet Format](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-3) + /// + /// This represents the sum of all [`NameEntry`] structures when encoded + /// into [`SftpSink`] format. The length is to be pre-computed by + /// encoding each entry and summing the [`SftpSink::payload_len()`] values. + fn get_encoded_len(&self) -> SftpOpResult { + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } + + /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter + /// were a [`NameEntry`] has been encoded. + /// + /// + fn for_each_encoded(&self, mut writer: F) -> SftpOpResult<()> + where + F: FnMut(&[u8]) -> (), + { + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } +} + // TODO Define this pub struct ReadReply<'g, 'a> { chan: ChanOut<'g, 'a>, @@ -99,15 +136,38 @@ impl<'g, 'a> ReadReply<'g, 'a> { // TODO Define this pub struct DirReply<'g, 'a> { + req_id: ReqId, + muting: &'a mut u32, chan: ChanOut<'g, 'a>, } impl<'g, 'a> DirReply<'g, 'a> { - pub fn reply(self, _data: &[u8]) {} + /// I am faking a DirReply to prototype it + pub fn mock(req_id: ReqId, muting: &'a mut u32) -> Self { + DirReply { + chan: ChanOut { _phantom_g: PhantomData, _phantom_a: PhantomData }, + muting, + req_id, + } + } + + /// mocks sending an item via a stdio + pub fn send_item(&mut self, data: &[u8]) { + *self.muting += 1; + debug!("Muted incremented {:?}. Got data: {:?}", self.muting, data); + } + + /// Must be call it first. Make this enforceable + pub fn send_header(&self, get_count: u32, get_encoded_len: u32) { + debug!( + "I will send the header here for request id {:?}: count = {:?}, length = {:?}", + self.req_id, get_count, get_encoded_len + ); + } } // TODO Implement correct Channel Out pub struct ChanOut<'g, 'a> { - _phantom_g: PhantomData<&'g ()>, - _phantom_a: PhantomData<&'a ()>, + _phantom_g: PhantomData<&'g ()>, // 'g look what these might be ChanIO lifetime + _phantom_a: PhantomData<&'a ()>, // a' Why the second lifetime if ChanIO only needs one } diff --git a/sftp/src/sftpsink.rs b/sftp/src/sftpsink.rs index ce55d265..377178c2 100644 --- a/sftp/src/sftpsink.rs +++ b/sftp/src/sftpsink.rs @@ -35,7 +35,7 @@ impl<'g> SftpSink<'g> { warn!("SftpSink trying to terminate it before pushing data"); return 0; } // size is 0 - let used_size = (self.index - SFTP_FIELD_LEN_LENGTH) as u32; + let used_size = self.payload_len() as u32; used_size .to_be_bytes() @@ -45,6 +45,16 @@ impl<'g> SftpSink<'g> { self.index } + + /// Auxiliary method to allow seen the len used by the encoded payload + pub fn payload_len(&self) -> usize { + self.index - SFTP_FIELD_LEN_LENGTH + } + + /// Auxiliary method to allow an immutable reference to the encoded payload + pub fn payload_slice(&self) -> &[u8] { + &self.buffer[SFTP_FIELD_LEN_LENGTH..self.payload_len()] + } } impl<'g> SSHSink for SftpSink<'g> { From 6b48d002fdfab73df50d88a97ea7e4ae707f48d7 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 16 Oct 2025 12:30:19 +1100 Subject: [PATCH 321/393] [skip ci] WIP: Refactored constant --- demo/sftp/std/src/demosftpserver.rs | 5 ++--- sftp/src/lib.rs | 4 ++++ sftp/src/proto.rs | 12 +++++++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index e4688d2b..f209da14 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -5,6 +5,7 @@ use crate::{ use sunset::sshwire::SSHEncode; use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; +use sunset_sftp::protocol::constants::MAX_NAME_ENTRY_SIZE; use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; use sunset_sftp::server::{ DirEntriesResponseHelpers, DirReply, ReadReply, SftpOpResult, SftpServer, @@ -350,8 +351,6 @@ impl DirEntriesCollection { attrs: Self::get_attrs_or_empty(entry.metadata()), }; - const MAX_NAME_ENTRY_SIZE: usize = 4 + 256 + 4 + 72; // 4 + 256 bytes for path, 4 for empty long path and 72 bytes for the attributes ( 32/4*7 + 64/4 * 1 = 72) - let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; let mut sftp_sink = SftpSink::new(&mut buffer); name_entry.enc(&mut sftp_sink).ok()?; @@ -423,7 +422,7 @@ impl DirEntriesResponseHelpers for DirEntriesCollection { attrs, }; debug!("Sending new item: {:?}", name_entry); - let mut buffer = [0u8; 4 + 256 + 4 + 72]; // 4 + 256 bytes for path, 4 for empty long path and 72 bytes for the attributes ( 32/4*7 + 64/4 * 1 = 72) + let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; let mut sftp_sink = SftpSink::new(&mut buffer); name_entry.enc(&mut sftp_sink).map_err(|err| { debug!("WireError: {:?}", err); diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 496bd475..0a9b81b0 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -94,6 +94,10 @@ pub mod protocol { pub use crate::proto::NameEntry; pub use crate::proto::PathInfo; pub use crate::proto::StatusCode; + /// Constants that might be useful for SFTP developers + pub mod constants { + pub use crate::proto::MAX_NAME_ENTRY_SIZE; + } } /// Errors and results used in this crate diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index de466721..24101601 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -27,15 +27,21 @@ pub const SFTP_FIELD_LEN_LENGTH: usize = 4; // SSH_FXP_WRITE SFTP Packet definition used to decode long packets that do not fit in one buffer -/// SFTP SSH_FXP_WRITE Packet cannot be shorter than this (len:4+pnum:1+rid:4+hand:4+0+data:4+0 bytes = 17 bytes) [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) +/// SFTP SSH_FXP_WRITE Packet cannot be shorter than this (len:4+pnum:1+rid:4+hand:4+0+data:4+0 bytes = 17 bytes) [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) // pub const SFTP_MINIMUM_WRITE_PACKET_LEN: usize = 17; -/// SFTP SSH_FXP_WRITE Packet request id field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) +/// SFTP SSH_FXP_WRITE Packet request id field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) pub const SFTP_WRITE_REQID_INDEX: usize = 5; -/// SFTP SSH_FXP_WRITE Packet handle field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-10) +/// SFTP SSH_FXP_WRITE Packet handle field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) // pub const SFTP_WRITE_HANDLE_INDEX: usize = 9; +/// Considering the definition in [Section 7](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) +/// for `SSH_FXP_READDIR` +/// +/// (4 + 256) bytes for path, (4 + 0) bytes for empty long path and 72 bytes for the attributes ( 32/4*7 + 64/4 * 1 = 72) +pub const MAX_NAME_ENTRY_SIZE: usize = 4 + 256 + 4 + 72; + // TODO is utf8 enough, or does this need to be an opaque binstring? /// See [SSH_FXP_NAME in Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) #[derive(Debug, SSHEncode, SSHDecode)] From 49a769d16650bf4c3b6cb48ea1f8b94b11209072 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 17 Oct 2025 14:18:02 +1100 Subject: [PATCH 322/393] [skip ci] WIP: Reworking a wrapper for Sink and ChanOut I want to use them grouped to later pass them to DirReply Also I might be able to use better the buffer out [skip ci] WIP: fixing some comments and warnings [skip ci] WIP: Ugly explorative refactoring [skip ci] WIP: Ugly explorative refactoring now working It is really ugly --- demo/sftp/std/src/demosftpserver.rs | 2 - demo/sftp/std/src/main.rs | 10 +- sftp/src/proto.rs | 3 +- sftp/src/sftphandler.rs | 326 ++++++++++++++++++---------- sftp/src/sftpserver.rs | 41 +++- sftp/src/sftpsink.rs | 29 ++- sftp/src/sftpsource.rs | 1 + 7 files changed, 289 insertions(+), 123 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index f209da14..5c6818ed 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -308,8 +308,6 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { name_entry_collection .for_each_encoded(|data: &[u8]| visitor.send_item(data))?; - - // debug!("got iterator = {:?}", name_entry_collection); } else { error!("the path is not a directory = {:?}", dir_path); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 74fccb3a..4de14d78 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -10,8 +10,6 @@ use crate::{ demoopaquefilehandle::DemoOpaqueFileHandle, demosftpserver::DemoSftpServer, }; -use embedded_io_async::{Read, Write}; - use embassy_executor::Spawner; use embassy_net::{Stack, StackResources, StaticConfigV4}; @@ -151,10 +149,10 @@ impl DemoServer for StdDemo { // TODO Do some research to find reasonable default buffer lengths let mut buffer_in = [0u8; 512]; let mut buffer_out = [0u8; 384]; - let mut incomplete_request_buffer = [0u8; 128]; // TODO Find a non arbitrary length + let mut incomplete_request_buffer = [0u8; 128]; match { - let mut stdio = serv.stdio(ch).await?; + let stdio = serv.stdio(ch).await?; let mut file_server = DemoSftpServer::new( "./demo/sftp/std/testing/out/".to_string(), ); @@ -164,8 +162,9 @@ impl DemoServer for StdDemo { &mut file_server, &mut incomplete_request_buffer, ); + sftp_handler - .process_loop(&mut stdio, &mut buffer_in, &mut buffer_out) + .process_loop(stdio, &mut buffer_in, &mut buffer_out) .await?; Ok::<_, Error>(()) @@ -212,6 +211,7 @@ async fn main(spawner: Spawner) { env_logger::builder() .filter_level(log::LevelFilter::Debug) .filter_module("sunset::runner", log::LevelFilter::Info) + // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Trace) .filter_module("sunset::traffic", log::LevelFilter::Info) .filter_module("sunset::encrypt", log::LevelFilter::Info) .filter_module("sunset::conn", log::LevelFilter::Info) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 24101601..a1bddeed 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -688,7 +688,7 @@ macro_rules! sftpmessages { } - /// Decode a request. Includes Initialization packets + /// Decode a request or initialization packets /// /// Used by a SFTP server. Does not include the length field. /// @@ -792,6 +792,5 @@ sftpmessages! [ (102, Handle, Handle<'a>, "ssh_fxp_handle"), (103, Data, Data<'a>, "ssh_fxp_data"), (104, Name, Name<'a>, "ssh_fxp_name"), - }, ]; diff --git a/sftp/src/sftphandler.rs b/sftp/src/sftphandler.rs index 27148fde..2c95da8f 100644 --- a/sftp/src/sftphandler.rs +++ b/sftp/src/sftphandler.rs @@ -12,8 +12,8 @@ use crate::sftpsink::SftpSink; use crate::sftpsource::SftpSource; use sunset::Error as SunsetError; -use sunset::sshwire::{SSHSource, WireError, WireResult}; -use sunset_async::ChanInOut; +use sunset::sshwire::{SSHEncode, SSHSource, WireError, WireResult}; +use sunset_async::{ChanInOut, ChanOut}; use core::u32; use embedded_io_async::{Read, Write}; @@ -39,7 +39,11 @@ enum FragmentedRequestState { /// and more bytes are needed ProcessingClippedRequest, /// A request, with a length over the incoming buffer capacity is being - /// processed + /// processed. + /// + /// E.g. a write request with size exceeding the + /// buffer size: Processing this request will require to be split + /// into multiple write actions ProcessingLongRequest, } @@ -126,6 +130,8 @@ where } } + /// WIP: A version of process that takes a chan_out to write the data + /// /// Maintaining an internal status: /// /// - Decodes the buffer_in request @@ -135,15 +141,13 @@ where /// /// **Returns**: A result containing the number of bytes used in /// `buffer_out` - pub async fn process( + async fn process<'g>( &mut self, buffer_in: &[u8], - buffer_out: &mut [u8], - ) -> SftpResult { + output_wrapper: &mut OutputWrapper<'a, 'g>, + ) -> SftpResult<()> { let in_len = buffer_in.len(); - let mut buffer_in_remaining_index = 0; - - let mut used_out_accumulated_index = 0; + let mut buffer_in_lower_index_bracket = 0; trace!("Received {:} bytes to process", in_len); @@ -153,23 +157,25 @@ where return Err(WireError::PacketWrong.into()); } - while buffer_in_remaining_index < in_len { - let mut sink = - SftpSink::new(&mut buffer_out[used_out_accumulated_index..]); - + while buffer_in_lower_index_bracket < in_len { + debug!( + "Buffer In Lower index bracket: {}", + buffer_in_lower_index_bracket + ); debug!( "<=======================[ SFTP Process State: {:?} ]=======================>", self.state ); match &self.state { + // There is a fragmented request in process of processing SftpHandleState::Fragmented(fragment_case) => { match fragment_case { FragmentedRequestState::ProcessingClippedRequest => { if let Err(e) = self .incomplete_request_holder .try_append_for_valid_request( - &buffer_in[buffer_in_remaining_index..], + &buffer_in[buffer_in_lower_index_bracket..], ) { match e { @@ -178,7 +184,7 @@ where "There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); - buffer_in_remaining_index += self + buffer_in_lower_index_bracket += self .incomplete_request_holder .appended(); continue; @@ -190,7 +196,7 @@ where "WIRE ERROR: There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); - buffer_in_remaining_index += self + buffer_in_lower_index_bracket += self .incomplete_request_holder .appended(); continue; @@ -216,7 +222,7 @@ where } let used = self.incomplete_request_holder.appended(); - buffer_in_remaining_index += used; + buffer_in_lower_index_bracket += used; let mut source = SftpSource::new( &self.incomplete_request_holder.try_get_ref()?, @@ -227,39 +233,37 @@ where Ok(request) => { Self::handle_general_request( &mut self.file_server, - &mut sink, + output_wrapper, request, - )?; + ) + .await?; self.incomplete_request_holder.reset(); self.state = SftpHandleState::Idle; } Err(e) => match e { - WireError::RanOut => { - match Self::handle_ran_out( - &mut self.file_server, - &mut sink, - &mut source, - ) { - Ok(holder) => { - self.partial_write_request_tracker = - Some(holder); - self.incomplete_request_holder - .reset(); - self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); - } - Err(e) => match e { - _ => { - error!( - "handle_ran_out finished with error: {:?}", - e - ); - return Err( - SunsetError::Bug.into() - ); - } - }, + WireError::RanOut => match Self::handle_ran_out( + &mut self.file_server, + output_wrapper, + &mut source, + ) + .await + { + Ok(holder) => { + self.partial_write_request_tracker = + Some(holder); + self.incomplete_request_holder.reset(); + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); } - } + Err(e) => match e { + _ => { + error!( + "handle_ran_out finished with error: {:?}", + e + ); + return Err(SunsetError::Bug.into()); + } + }, + }, WireError::NoRoom => { error!("Not enough space to fit the request") } @@ -275,7 +279,7 @@ where } FragmentedRequestState::ProcessingLongRequest => { let mut source = SftpSource::new( - &buffer_in[buffer_in_remaining_index..], + &buffer_in[buffer_in_lower_index_bracket..], ); trace!("Source content: {:?}", source); @@ -333,7 +337,11 @@ where self.partial_write_request_tracker = Some(write_tracker); } else { - push_ok(write_tracker.req_id, &mut sink)?; + push_ok( + write_tracker.req_id, + &mut output_wrapper.get_mut_sink_ref(), + )?; + output_wrapper.send_buffer().await?; info!("Finished multi part Write Request"); self.state = SftpHandleState::Idle; } @@ -343,29 +351,26 @@ where push_general_failure( write_tracker.req_id, "error writing", - &mut sink, + &mut output_wrapper.get_mut_sink_ref(), )?; + output_wrapper.send_buffer().await?; self.state = SftpHandleState::Idle; } }; - buffer_in_remaining_index = in_len - source.remaining(); + buffer_in_lower_index_bracket = + in_len - source.remaining(); } } } + // No pending request _ => { let mut source = - SftpSource::new(&buffer_in[buffer_in_remaining_index..]); - trace!("Source content: {:?}", source); + SftpSource::new(&buffer_in[buffer_in_lower_index_bracket..]); let sftp_packet = SftpPacket::decode_request(&mut source); match self.state { - SftpHandleState::Fragmented(_) => { - return Err( - SftpError::SunsetError(SunsetError::Bug).into() - ); - } SftpHandleState::Initializing => match sftp_packet { Ok(request) => { match request { @@ -375,8 +380,11 @@ where version: SFTP_VERSION, }); - info!("Sending '{:?}'", version); - version.encode_response(&mut sink)?; + // info!("Sending '{:?}'", version); + version.encode_response( + output_wrapper.get_mut_sink_ref(), + )?; + output_wrapper.send_buffer().await?; self.state = SftpHandleState::Idle; } _ => { @@ -399,12 +407,12 @@ where SftpHandleState::Idle => { match sftp_packet { Ok(request) => { - // self.handle_general_request(&mut sink, request)? Self::handle_general_request( &mut self.file_server, - &mut sink, + output_wrapper, request, - )?; + ) + .await?; } Err(e) => match e { WireError::RanOut => { @@ -415,9 +423,11 @@ where match Self::handle_ran_out( &mut self.file_server, - &mut sink, + output_wrapper, &mut source, - ) { + ) + .await + { Ok(holder) => { self.partial_write_request_tracker = Some(holder); @@ -433,9 +443,9 @@ where let read = self.incomplete_request_holder .try_hold( &buffer_in - [buffer_in_remaining_index..], + [buffer_in_lower_index_bracket..], )?; - buffer_in_remaining_index += + buffer_in_lower_index_bracket += read; self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); continue; @@ -449,13 +459,6 @@ where } }; } - WireError::UnknownPacket { number: _ } => { - warn!("Error decoding SFTP Packet:{:?}", e); - push_unsupported( - ReqId(u32::MAX), - &mut sink, - )?; - } _ => { error!( "Error decoding SFTP Packet: {:?}", @@ -463,63 +466,62 @@ where ); push_unsupported( ReqId(u32::MAX), - &mut sink, + &mut output_wrapper.get_mut_sink_ref(), )?; + output_wrapper.send_buffer().await?; } }, }; } + _ => { + error!( + "Unhandled SftpHandleState {:?} in main loop", + self.state + ); + return Err(SunsetError::Bug.into()); + } } - buffer_in_remaining_index = in_len - source.remaining(); + buffer_in_lower_index_bracket = in_len - source.remaining(); } }; - used_out_accumulated_index += sink.finalize(); } - Ok(used_out_accumulated_index) + Ok(()) } /// WIP: A loop that will process all the request from stdio until /// an EOF is received pub async fn process_loop<'c>( &mut self, - stdio: &mut ChanInOut<'c>, + stdio: ChanInOut<'c>, buffer_in: &mut [u8], - buffer_out: &mut [u8], + buffer_out: &'a mut [u8], ) -> SftpResult<()> { + let (mut chan_in, chan_out) = stdio.split(); + + let mut out_wrapper = OutputWrapper::new(buffer_out, chan_out); loop { - // let channelspub struct ChanOut<'g>(ChanIO<'g>) = stdio.split(); - let lr = stdio.read(buffer_in).await?; + let lr = chan_in.read(buffer_in).await?; trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); if lr == 0 { debug!("client disconnected"); return Err(SftpError::ClientDisconnected); } - let lw = self.process(&buffer_in[0..lr], buffer_out).await?; - if lw > 0 { - let wo = stdio.write(&mut buffer_out[0..lw]).await?; - if wo != lw { - error!("SFTP ----> Sent incomplete {} < {}", wo, lw); - todo!( - "Fix write incomplete condition: Repeat until \ - all is gone down the channel" - ); - } - trace!("SFTP ----> Sent: {:?}", &buffer_out[0..lw]); - } + self.process(&buffer_in[0..lr], &mut out_wrapper).await?; } - Ok(()) + // Ok(()) } - fn handle_general_request( + async fn handle_general_request<'g>( file_server: &mut S, - sink: &mut SftpSink<'_>, + output_wrapper: &mut OutputWrapper<'a, 'g>, request: SftpPacket<'_>, ) -> Result<(), SftpError> where T: OpaqueFileHandle, { + debug!("Handling general request: {:?}", request); match request { SftpPacket::Init(_) => { error!("The Init packet is not a request but an initialization"); @@ -529,8 +531,14 @@ where let a_name = file_server.realpath(path_info.path.as_str()?)?; let response = SftpPacket::Name(req_id, a_name); + debug!( + "Request Id {:?}. Encoding response: {:?}", + &req_id, &response + ); - response.encode_response(sink)?; + response.encode_response(output_wrapper.get_mut_sink_ref())?; + // dbg!("PathInfo response encoded", &response); + output_wrapper.send_buffer().await?; } SftpPacket::Open(req_id, open) => { match file_server.open(open.filename.as_str()?, &open.attrs) { @@ -541,34 +549,58 @@ where handle: opaque_file_handle.into_file_handle(), }, ); - response.encode_response(sink)?; - info!("Sending '{:?}'", response); + response + .encode_response(output_wrapper.get_mut_sink_ref())?; + output_wrapper.send_buffer().await?; } Err(status_code) => { error!("Open failed: {:?}", status_code); - push_general_failure(req_id, "", sink)?; + push_general_failure( + req_id, + "", + output_wrapper.get_mut_sink_ref(), + )?; + output_wrapper.send_buffer().await?; } }; } + // TODO The visitor behavioral pattern could be use in write to speed-up + // the writing process SftpPacket::Write(req_id, write) => { match file_server.write( &T::try_from(&write.handle)?, write.offset, write.data.as_ref(), ) { - Ok(_) => push_ok(req_id, sink)?, + Ok(_) => { + push_ok(req_id, output_wrapper.get_mut_sink_ref())?; + output_wrapper.send_buffer().await?; + } Err(e) => { error!("SFTP write thrown: {:?}", e); - push_general_failure(req_id, "error writing", sink)? + push_general_failure( + req_id, + "error writing", + output_wrapper.get_mut_sink_ref(), + )?; + output_wrapper.send_buffer().await?; } }; } SftpPacket::Close(req_id, close) => { match file_server.close(&T::try_from(&close.handle)?) { - Ok(_) => push_ok(req_id, sink)?, + Ok(_) => { + push_ok(req_id, output_wrapper.get_mut_sink_ref())?; + output_wrapper.send_buffer().await?; + } Err(e) => { error!("SFTP Close thrown: {:?}", e); - push_general_failure(req_id, "", sink)? + push_general_failure( + req_id, + "", + output_wrapper.get_mut_sink_ref(), + )?; + output_wrapper.send_buffer().await?; } } } @@ -581,12 +613,18 @@ where handle: opaque_file_handle.into_file_handle(), }, ); - response.encode_response(sink)?; - info!("Sending '{:?}'", response); + response + .encode_response(output_wrapper.get_mut_sink_ref())?; + output_wrapper.send_buffer().await?; } Err(status_code) => { error!("Open failed: {:?}", status_code); - push_general_failure(req_id, "", sink)?; + push_general_failure( + req_id, + "", + output_wrapper.get_mut_sink_ref(), + )?; + output_wrapper.send_buffer().await?; } }; } @@ -606,7 +644,8 @@ where } Err(status_code) => { error!("Open failed: {:?}", status_code); - push_unsupported(req_id, sink)?; + push_unsupported(req_id, output_wrapper.get_mut_sink_ref())?; + output_wrapper.send_buffer().await?; } }; debug!("final muting: {:?}", muting); @@ -615,7 +654,7 @@ where // push_unsupported(ReqId(0), sink)?; } _ => { - error!("Unsuported request type: {:?}", request); + error!("Unsupported request type: {:?}", request); return Err(SftpError::NotSupported); // push_unsupported(ReqId(0), sink)?; } @@ -628,9 +667,9 @@ where /// /// **WARNING:** Only `SSH_FXP_WRITE` has been implemented! /// - fn handle_ran_out( + async fn handle_ran_out<'g>( file_server: &mut S, - sink: &mut SftpSink<'_>, + output_wrapper: &mut OutputWrapper<'a, 'g>, source: &mut SftpSource<'_>, ) -> SftpResult> { let packet_type = source.peak_packet_type()?; @@ -673,7 +712,12 @@ where } Err(e) => { error!("SFTP write thrown: {:?}", e); - push_general_failure(req_id, "error writing ", sink)?; + push_general_failure( + req_id, + "error writing ", + output_wrapper.get_mut_sink_ref(), + )?; + output_wrapper.send_buffer().await?; return Err(SftpError::FileServerError(e)); } }; @@ -690,6 +734,68 @@ where } } +struct OutputWrapper<'a, 'g> { + sink: SftpSink<'a>, + channel_out: ChanOut<'g>, +} + +impl<'a, 'g> OutputWrapper<'a, 'g> { + pub fn new(buffer: &'a mut [u8], channel_out: ChanOut<'g>) -> Self { + let sink = SftpSink::new(buffer); + OutputWrapper { channel_out, sink } + } + + pub fn reset(&mut self) { + self.sink.reset(); + } + + pub fn get_mut_sink_ref(&mut self) -> &mut SftpSink<'a> { + &mut self.sink + } + + // TODO Are we using this? + pub fn encode(&mut self, data: &T) -> SftpResult<()> + where + T: SSHEncode, + { + data.enc(&mut self.sink)?; + Ok(()) + } + // + + /// Finalizes (Prepends the packet length) and send the data in the + /// buffer by the subsystem channel out + pub async fn send_buffer(&mut self) -> SftpResult { + if self.sink.payload_len() == 0 { + debug!("No data to send in the SFTP sink"); + return Ok(0); + } + self.sink.finalize(); + let buffer = self.sink.used_slice(); + info!("Sending buffer: '{:?}'", buffer); + let written = self.channel_out.write(buffer).await?; + self.sink.reset(); + Ok(written) + } + + /// Send the data in the buffer by the subsystem channel out without + /// prepending the packet length to it. + /// + /// This is useful when an SFTP packet header has already being sent + /// or when the data requires an special treatment + pub async fn send_payload(&mut self) -> SftpResult { + let payload = self.sink.payload_slice(); + info!("Sending payload: '{:?}'", payload); + let written = self.channel_out.write(payload).await?; + self.sink.reset(); + Ok(written) + } + + pub fn finalize(&mut self) -> usize { + self.sink.finalize() + } +} + #[inline] fn push_ok(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireError> { let response = SftpPacket::Status( diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 70e7fe35..895605c4 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -6,6 +6,7 @@ use crate::{ }; use core::marker::PhantomData; +// use futures::executor::block_on; TODO Deal with the async nature of [`ChanOut`] /// Result used to store the result of an Sftp Operation pub type SftpOpResult = core::result::Result; @@ -71,6 +72,7 @@ where } /// Reads the list of items in a directory + #[allow(unused_variables)] fn readdir( &mut self, opaque_dir_handle: &T, @@ -117,7 +119,8 @@ pub trait DirEntriesResponseHelpers { /// were a [`NameEntry`] has been encoded. /// /// - fn for_each_encoded(&self, mut writer: F) -> SftpOpResult<()> + #[allow(unused_variables)] + fn for_each_encoded(&self, writer: F) -> SftpOpResult<()> where F: FnMut(&[u8]) -> (), { @@ -126,17 +129,49 @@ pub trait DirEntriesResponseHelpers { } // TODO Define this +/// **This is a work in progress** +/// A reference structure passed to the [`SftpServer::read()`] method to +/// allow replying with the read data. + pub struct ReadReply<'g, 'a> { chan: ChanOut<'g, 'a>, } impl<'g, 'a> ReadReply<'g, 'a> { - pub fn reply(self, _data: &[u8]) {} + /// **This is a work in progress** + /// + /// Reply with a slice containing the read data + /// It can be called several times to send multiple data chunks + /// + /// **Important**: The first reply should contain the header + #[allow(unused_variables)] + pub fn reply(self, data: &[u8]) {} } // TODO Define this +/// Dir Reply is the structure that will be "visiting" the [`SftpServer`] +/// trait +/// implementation via [`SftpServer::readdir()`] in order to send the +/// directory content list. +/// +/// It contains references to a slice buffer and a output channel +/// [`sunset_async::async_channel::ChanOut`] used in the context of an +/// SFTP Session. +/// +/// The usage is simple: +/// +/// 1. SftpHandler will: Initialize the structure with the buffer and the ChanOut +/// 2. The `SftpServer` trait implementation for `readdir()` will: +/// a. Receive the DirReply +/// b. Obtain the number of items and the **total** encoded length of +/// the [`NameEntry`] items in the directory and send them calling `send_header` +/// c. Send each serialized `NameEntry`, excluding their length using +/// the `send_item` method +/// pub struct DirReply<'g, 'a> { + /// Used during the req_id: ReqId, + /// To test muting operations muting: &'a mut u32, chan: ChanOut<'g, 'a>, } @@ -151,12 +186,14 @@ impl<'g, 'a> DirReply<'g, 'a> { } } + // TODO this will need to do async execution /// mocks sending an item via a stdio pub fn send_item(&mut self, data: &[u8]) { *self.muting += 1; debug!("Muted incremented {:?}. Got data: {:?}", self.muting, data); } + // TODO this will need to do async execution /// Must be call it first. Make this enforceable pub fn send_header(&self, get_count: u32, get_encoded_len: u32) { debug!( diff --git a/sftp/src/sftpsink.rs b/sftp/src/sftpsink.rs index 377178c2..08fa7f9e 100644 --- a/sftp/src/sftpsink.rs +++ b/sftp/src/sftpsink.rs @@ -12,7 +12,7 @@ use log::{debug, error, info, log, trace, warn}; /// len #[derive(Default)] pub struct SftpSink<'g> { - pub buffer: &'g mut [u8], + buffer: &'g mut [u8], index: usize, } @@ -52,8 +52,32 @@ impl<'g> SftpSink<'g> { } /// Auxiliary method to allow an immutable reference to the encoded payload + /// excluding the `u32` length field prepended to it pub fn payload_slice(&self) -> &[u8] { - &self.buffer[SFTP_FIELD_LEN_LENGTH..self.payload_len()] + &self.buffer + [SFTP_FIELD_LEN_LENGTH..SFTP_FIELD_LEN_LENGTH + self.payload_len()] + } + + /// Auxiliary method to allow an immutable reference to the full used + /// data (includes the prepended length field) + /// + /// **Important:** Call this after [`SftpSink::finalize()`] + pub fn used_slice(&self) -> &[u8] { + debug!( + "SftpSink used_slice called, total len: {}. Index: {}", + SFTP_FIELD_LEN_LENGTH + self.payload_len(), + self.index + ); + &self.buffer[..SFTP_FIELD_LEN_LENGTH + self.payload_len()] + } + + /// Reset the index and cleans the length field + pub fn reset(&mut self) -> () { + debug!("SftpSink reset called when index was {:?}", self.index); + self.index = SFTP_FIELD_LEN_LENGTH; + for i in 0..SFTP_FIELD_LEN_LENGTH { + self.buffer[i] = 0; + } } } @@ -64,6 +88,7 @@ impl<'g> SSHSink for SftpSink<'g> { } trace!("Sink index: {:}", self.index); v.iter().for_each(|val| { + trace!("Writing val {:} at index {:}", *val, self.index); self.buffer[self.index] = *val; self.index += 1; }); diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 67b743e8..7e1918e3 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -47,6 +47,7 @@ impl<'de> SSHSource<'de> for SftpSource<'de> { impl<'de> SftpSource<'de> { /// Creates a new [`SftpSource`] referencing a buffer pub fn new(buffer: &'de [u8]) -> Self { + debug!("New source with content: : {:?}", buffer); SftpSource { buffer: buffer, index: 0 } } From 801086c09e58eb5408f16af57362f9c34001f5b3 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 22 Oct 2025 18:36:02 +1100 Subject: [PATCH 323/393] [skip ci] WIP: Refactoring. Creating a mod for sftphandler [skip ci] WIP: Refactoring. Normalizing push function Unifying around push_status [skip ci] WIP: Refactoring. Integrating push functions in OutputWrapper - push_status - push_packet [skip ci] WIP: Refactoring. Moving SftpOutputChannelWrapper (aka OutputWrapper) to its own module cleaning up sftphandle.rs [skip ci] WIP: Refactoring - SftpHandler: estate machine restructuring - SftpServer: WIP for DirReply - SftpOutputChannelWrapper: WIP refactoring --- demo/sftp/std/src/main.rs | 3 +- sftp/src/sftphandler/mod.rs | 7 + .../sftphandler/partialwriterequesttracker.rs | 60 ++ sftp/src/{ => sftphandler}/sftphandler.rs | 569 +++++++----------- .../sftphandler/sftpoutputchannelwrapper.rs | 94 +++ sftp/src/sftpserver.rs | 54 +- 6 files changed, 430 insertions(+), 357 deletions(-) create mode 100644 sftp/src/sftphandler/mod.rs create mode 100644 sftp/src/sftphandler/partialwriterequesttracker.rs rename sftp/src/{ => sftphandler}/sftphandler.rs (60%) create mode 100644 sftp/src/sftphandler/sftpoutputchannelwrapper.rs diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 4de14d78..4052cbb8 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -211,7 +211,8 @@ async fn main(spawner: Spawner) { env_logger::builder() .filter_level(log::LevelFilter::Debug) .filter_module("sunset::runner", log::LevelFilter::Info) - // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Trace) + .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) + .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) .filter_module("sunset::traffic", log::LevelFilter::Info) .filter_module("sunset::encrypt", log::LevelFilter::Info) .filter_module("sunset::conn", log::LevelFilter::Info) diff --git a/sftp/src/sftphandler/mod.rs b/sftp/src/sftphandler/mod.rs new file mode 100644 index 00000000..fc32ff97 --- /dev/null +++ b/sftp/src/sftphandler/mod.rs @@ -0,0 +1,7 @@ +mod partialwriterequesttracker; +mod sftphandler; +mod sftpoutputchannelwrapper; + +pub use partialwriterequesttracker::PartialWriteRequestTracker; +pub use sftphandler::SftpHandler; +pub use sftpoutputchannelwrapper::SftpOutputChannelWrapper; diff --git a/sftp/src/sftphandler/partialwriterequesttracker.rs b/sftp/src/sftphandler/partialwriterequesttracker.rs new file mode 100644 index 00000000..72acbfe4 --- /dev/null +++ b/sftp/src/sftphandler/partialwriterequesttracker.rs @@ -0,0 +1,60 @@ +use crate::handles::OpaqueFileHandle; +use crate::proto::ReqId; +use sunset::sshwire::WireResult; + +// TODO Generalize this to allow other request types +/// Used to keep record of a long SFTP Write request that does not fit in +/// receiving buffer and requires processing in batches +#[derive(Debug)] +pub struct PartialWriteRequestTracker { + req_id: ReqId, + opaque_handle: T, + remain_data_len: u32, + remain_data_offset: u64, +} + +impl PartialWriteRequestTracker { + /// Creates a new [`PartialWriteRequestTracker`] + pub fn new( + req_id: ReqId, + opaque_handle: T, + remain_data_len: u32, + remain_data_offset: u64, + ) -> WireResult { + Ok(PartialWriteRequestTracker { + req_id, + opaque_handle: opaque_handle, + remain_data_len, + remain_data_offset, + }) + } + /// Returns the opaque file handle associated with the request + /// tracked + pub fn get_opaque_file_handle(&self) -> T { + self.opaque_handle.clone() + } + + pub fn get_remain_data_len(&self) -> u32 { + self.remain_data_len + } + + pub fn get_remain_data_offset(&self) -> u64 { + self.remain_data_offset + } + + // pub fn add_to_remain_data_offset(&mut self, add_offset: u64) { + // self.remain_data_offset += add_offset; + // } + + pub(crate) fn update_remaining_after_partial_write( + &mut self, + data_segment_len: u32, + ) -> () { + self.remain_data_offset += data_segment_len as u64; + self.remain_data_len -= data_segment_len; + } + + pub(crate) fn get_req_id(&self) -> ReqId { + self.req_id.clone() // TODO reference? + } +} diff --git a/sftp/src/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs similarity index 60% rename from sftp/src/sftphandler.rs rename to sftp/src/sftphandler/sftphandler.rs index 2c95da8f..71969f4b 100644 --- a/sftp/src/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -1,22 +1,24 @@ +use super::PartialWriteRequestTracker; + use crate::error::SftpError; use crate::handles::OpaqueFileHandle; use crate::proto::{ self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, - SftpPacket, Status, StatusCode, + SftpPacket, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; use crate::server::DirReply; use crate::sftperror::SftpResult; +use crate::sftphandler::sftpoutputchannelwrapper::SftpOutputChannelWrapper; use crate::sftpserver::SftpServer; -use crate::sftpsink::SftpSink; use crate::sftpsource::SftpSource; use sunset::Error as SunsetError; -use sunset::sshwire::{SSHEncode, SSHSource, WireError, WireResult}; -use sunset_async::{ChanInOut, ChanOut}; +use sunset::sshwire::{SSHSource, WireError}; +use sunset_async::ChanInOut; use core::u32; -use embedded_io_async::{Read, Write}; +use embedded_io_async::Read; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; @@ -47,38 +49,38 @@ enum FragmentedRequestState { ProcessingLongRequest, } -// TODO Generalize this to allow other request types -/// Used to keep record of a long SFTP Write request that does not fit in -/// receiving buffer and requires processing in batches -#[derive(Debug)] -pub struct PartialWriteRequestTracker { - req_id: ReqId, - obscure_file_handle: T, - remain_data_len: u32, - remain_data_offset: u64, -} - -impl PartialWriteRequestTracker { - /// Creates a new [`PartialWriteRequestTracker`] - pub fn new( - req_id: ReqId, - obscure_file_handle: T, - remain_data_len: u32, - remain_data_offset: u64, - ) -> WireResult { - Ok(PartialWriteRequestTracker { - req_id, - obscure_file_handle: obscure_file_handle, - remain_data_len, - remain_data_offset, - }) - } - /// Returns the opaque file handle associated with the request - /// tracked - pub fn get_opaque_file_handle(&self) -> T { - self.obscure_file_handle.clone() - } -} +// // TODO Generalize this to allow other request types +// /// Used to keep record of a long SFTP Write request that does not fit in +// /// receiving buffer and requires processing in batches +// #[derive(Debug)] +// pub struct PartialWriteRequestTracker { +// req_id: ReqId, +// opaque_handle: T, +// remain_data_len: u32, +// remain_data_offset: u64, +// } + +// impl PartialWriteRequestTracker { +// /// Creates a new [`PartialWriteRequestTracker`] +// pub fn new( +// req_id: ReqId, +// opaque_handle: T, +// remain_data_len: u32, +// remain_data_offset: u64, +// ) -> WireResult { +// Ok(PartialWriteRequestTracker { +// req_id, +// opaque_handle: opaque_handle, +// remain_data_len, +// remain_data_offset, +// }) +// } +// /// Returns the opaque file handle associated with the request +// /// tracked +// pub fn get_opaque_file_handle(&self) -> T { +// self.opaque_handle.clone() +// } +// } /// Process the raw buffers in and out from a subsystem channel decoding /// request and encoding responses @@ -144,7 +146,7 @@ where async fn process<'g>( &mut self, buffer_in: &[u8], - output_wrapper: &mut OutputWrapper<'a, 'g>, + output_wrapper: &mut SftpOutputChannelWrapper<'a, 'g>, ) -> SftpResult<()> { let in_len = buffer_in.len(); let mut buffer_in_lower_index_bracket = 0; @@ -202,9 +204,12 @@ where continue; } RequestHolderError::NoRoom => { - warn!( - "The request holder if full but the request in incomplete" - ) + error!( + "The request holder if full but the request in incomplete. \ + Consider increasing its size" + ); + // TODO react to this situation with an internal server error + return Err(SunsetError::NoRoom {}.into()); } _ => { @@ -299,7 +304,7 @@ where let usable_data = source .remaining() - .min(write_tracker.remain_data_len as usize); + .min(write_tracker.get_remain_data_len() as usize); let data_segment = // Fails!! source.dec_as_binstring(usable_data)?; @@ -312,10 +317,10 @@ where SunsetError::Bug })?; let current_write_offset = - write_tracker.remain_data_offset; - write_tracker.remain_data_offset += - data_segment_len as u64; - write_tracker.remain_data_len -= data_segment_len; + write_tracker.get_remain_data_offset(); + write_tracker.update_remaining_after_partial_write( + data_segment_len, + ); debug!( "Processing successive chunks of a long write packet. \ @@ -324,7 +329,7 @@ where opaque_handle, current_write_offset, data_segment, - write_tracker.remain_data_len + write_tracker.get_remain_data_len() ); match self.file_server.write( @@ -333,27 +338,30 @@ where data_segment.as_ref(), ) { Ok(_) => { - if write_tracker.remain_data_len > 0 { + if write_tracker.get_remain_data_len() > 0 { self.partial_write_request_tracker = Some(write_tracker); } else { - push_ok( - write_tracker.req_id, - &mut output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status( + write_tracker.get_req_id(), + StatusCode::SSH_FX_OK, + "", + ) + .await?; info!("Finished multi part Write Request"); self.state = SftpHandleState::Idle; } } Err(e) => { error!("SFTP write thrown: {:?}", e); - push_general_failure( - write_tracker.req_id, - "error writing", - &mut output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status( + write_tracker.get_req_id(), + StatusCode::SSH_FX_FAILURE, + "error writing", + ) + .await?; self.state = SftpHandleState::Idle; } }; @@ -363,127 +371,114 @@ where } } - // No pending request - _ => { - let mut source = - SftpSource::new(&buffer_in[buffer_in_lower_index_bracket..]); - - let sftp_packet = SftpPacket::decode_request(&mut source); - - match self.state { - SftpHandleState::Initializing => match sftp_packet { - Ok(request) => { - match request { - SftpPacket::Init(_) => { - let version = - SftpPacket::Version(InitVersionLowest { - version: SFTP_VERSION, - }); - - // info!("Sending '{:?}'", version); - version.encode_response( - output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; - self.state = SftpHandleState::Idle; - } - _ => { - error!( - "Request received before init: {:?}", - request - ); - return Err(SftpError::NotInitialized); - } - }; - } - Err(_) => { - error!( - "Malformed SFTP Packet before Init: {:?}", - sftp_packet - ); - return Err(SftpError::MalformedPacket); - } - }, - SftpHandleState::Idle => { - match sftp_packet { - Ok(request) => { - Self::handle_general_request( - &mut self.file_server, - output_wrapper, - request, - ) - .await?; + SftpHandleState::Initializing => { + let (source, sftp_packet) = create_sftp_source_and_packet( + buffer_in, + buffer_in_lower_index_bracket, + ); + match sftp_packet { + Ok(request) => { + match request { + SftpPacket::Init(_) => { + let version = + SftpPacket::Version(InitVersionLowest { + version: SFTP_VERSION, + }); + + output_wrapper.send_packet(version).await?; + self.state = SftpHandleState::Idle; } - Err(e) => match e { - WireError::RanOut => { - warn!( - "RanOut for the SFTP Packet in the source buffer: {:?}", - e - ); + _ => { + error!( + "Request received before init: {:?}", + request + ); + return Err(SftpError::NotInitialized); + } + }; + } + Err(_) => { + error!( + "Malformed SFTP Packet before Init: {:?}", + sftp_packet + ); + return Err(SftpError::MalformedPacket); + } + } + buffer_in_lower_index_bracket = in_len - source.remaining(); + } + SftpHandleState::Idle => { + let (mut source, sftp_packet) = create_sftp_source_and_packet( + buffer_in, + buffer_in_lower_index_bracket, + ); + match sftp_packet { + Ok(request) => { + Self::handle_general_request( + &mut self.file_server, + output_wrapper, + request, + ) + .await?; + } + Err(e) => match e { + WireError::RanOut => { + warn!( + "RanOut for the SFTP Packet in the source buffer: {:?}", + e + ); - match Self::handle_ran_out( - &mut self.file_server, - output_wrapper, - &mut source, - ) - .await - { - Ok(holder) => { - self.partial_write_request_tracker = - Some(holder); - self.state = + match Self::handle_ran_out( + &mut self.file_server, + output_wrapper, + &mut source, + ) + .await + { + Ok(holder) => { + self.partial_write_request_tracker = + Some(holder); + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) - } - Err(e) => { - error!("Error handle_ran_out"); - match e { - SftpError::WireError( - WireError::RanOut, - ) => { - let read = self.incomplete_request_holder + } + Err(e) => { + error!("Error handle_ran_out"); + match e { + SftpError::WireError( + WireError::RanOut, + ) => { + let read = self.incomplete_request_holder .try_hold( &buffer_in [buffer_in_lower_index_bracket..], )?; - buffer_in_lower_index_bracket += - read; - self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); - continue; - } - _ => { - return Err( - SunsetError::Bug.into(), - ); - } - } + buffer_in_lower_index_bracket += + read; + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); + continue; } - }; - } - _ => { - error!( - "Error decoding SFTP Packet: {:?}", - e - ); - push_unsupported( - ReqId(u32::MAX), - &mut output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + _ => { + return Err(SunsetError::Bug.into()); + } + } } - }, - }; - } - _ => { - error!( - "Unhandled SftpHandleState {:?} in main loop", - self.state - ); - return Err(SunsetError::Bug.into()); - } - } + }; + } + _ => { + error!("Error decoding SFTP Packet: {:?}", e); + output_wrapper + .send_status( + ReqId(u32::MAX), + StatusCode::SSH_FX_OP_UNSUPPORTED, + "Error decoding SFTP Packet", + ) + .await?; + } + }, + }; buffer_in_lower_index_bracket = in_len - source.remaining(); } - }; + } } Ok(()) @@ -499,7 +494,8 @@ where ) -> SftpResult<()> { let (mut chan_in, chan_out) = stdio.split(); - let mut out_wrapper = OutputWrapper::new(buffer_out, chan_out); + let mut chan_out_wrapper = + SftpOutputChannelWrapper::new(buffer_out, chan_out); loop { let lr = chan_in.read(buffer_in).await?; trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); @@ -508,14 +504,13 @@ where return Err(SftpError::ClientDisconnected); } - self.process(&buffer_in[0..lr], &mut out_wrapper).await?; + self.process(&buffer_in[0..lr], &mut chan_out_wrapper).await?; } - // Ok(()) } async fn handle_general_request<'g>( file_server: &mut S, - output_wrapper: &mut OutputWrapper<'a, 'g>, + output_wrapper: &mut SftpOutputChannelWrapper<'a, 'g>, request: SftpPacket<'_>, ) -> Result<(), SftpError> where @@ -536,9 +531,7 @@ where &req_id, &response ); - response.encode_response(output_wrapper.get_mut_sink_ref())?; - // dbg!("PathInfo response encoded", &response); - output_wrapper.send_buffer().await?; + output_wrapper.send_packet(response).await?; } SftpPacket::Open(req_id, open) => { match file_server.open(open.filename.as_str()?, &open.attrs) { @@ -549,18 +542,13 @@ where handle: opaque_file_handle.into_file_handle(), }, ); - response - .encode_response(output_wrapper.get_mut_sink_ref())?; - output_wrapper.send_buffer().await?; + output_wrapper.send_packet(response).await?; } Err(status_code) => { error!("Open failed: {:?}", status_code); - push_general_failure( - req_id, - "", - output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status(req_id, StatusCode::SSH_FX_FAILURE, "") + .await?; } }; } @@ -573,34 +561,38 @@ where write.data.as_ref(), ) { Ok(_) => { - push_ok(req_id, output_wrapper.get_mut_sink_ref())?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status(req_id, StatusCode::SSH_FX_OK, "") + .await?; } Err(e) => { error!("SFTP write thrown: {:?}", e); - push_general_failure( - req_id, - "error writing", - output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status( + req_id, + StatusCode::SSH_FX_FAILURE, + "error writing", + ) + .await?; } }; } SftpPacket::Close(req_id, close) => { match file_server.close(&T::try_from(&close.handle)?) { Ok(_) => { - push_ok(req_id, output_wrapper.get_mut_sink_ref())?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status(req_id, StatusCode::SSH_FX_OK, "") + .await?; } Err(e) => { error!("SFTP Close thrown: {:?}", e); - push_general_failure( - req_id, - "", - output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status( + req_id, + StatusCode::SSH_FX_FAILURE, + "Could not Close the handle", + ) + .await?; } } } @@ -613,18 +605,13 @@ where handle: opaque_file_handle.into_file_handle(), }, ); - response - .encode_response(output_wrapper.get_mut_sink_ref())?; - output_wrapper.send_buffer().await?; + output_wrapper.send_packet(response).await?; } Err(status_code) => { error!("Open failed: {:?}", status_code); - push_general_failure( - req_id, - "", - output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status(req_id, StatusCode::SSH_FX_FAILURE, "") + .await?; } }; } @@ -632,9 +619,7 @@ where // TODO Implement the mechanism you are going to use to // handle the list of elements - let mut muting = 0; - - let mut dir_reply = DirReply::mock(req_id, &mut muting); + let mut dir_reply = DirReply::new(req_id, output_wrapper); match file_server .readdir(&T::try_from(&read_dir.handle)?, &mut dir_reply) @@ -644,11 +629,15 @@ where } Err(status_code) => { error!("Open failed: {:?}", status_code); - push_unsupported(req_id, output_wrapper.get_mut_sink_ref())?; - output_wrapper.send_buffer().await?; + // output_wrapper + // .push_status( + // req_id, + // StatusCode::SSH_FX_OP_UNSUPPORTED, + // "Error Reading Directory", + // ) + // .await?; } }; - debug!("final muting: {:?}", muting); error!("Unsupported Read Dir : {:?}", read_dir); // return Err(SftpError::NotSupported); // push_unsupported(ReqId(0), sink)?; @@ -663,13 +652,21 @@ where } // TODO Handle more long requests - /// Handles long request that do not fit in the buffers and stores a tracker + /// Some long request will not fit in the channel buffers. Such requests + /// will require to be handled differently. Gathering the data in and + /// processing it as we receive it in the channel in buffer. + /// + /// In the current approach a tracker is required to store the state of + /// the processing of such long requests. + /// + /// With an implementation that where able to hold the channel_in there might + /// be no need to keep this tracker. /// /// **WARNING:** Only `SSH_FXP_WRITE` has been implemented! /// async fn handle_ran_out<'g>( file_server: &mut S, - output_wrapper: &mut OutputWrapper<'a, 'g>, + output_wrapper: &mut SftpOutputChannelWrapper<'a, 'g>, source: &mut SftpSource<'_>, ) -> SftpResult> { let packet_type = source.peak_packet_type()?; @@ -712,12 +709,13 @@ where } Err(e) => { error!("SFTP write thrown: {:?}", e); - push_general_failure( - req_id, - "error writing ", - output_wrapper.get_mut_sink_ref(), - )?; - output_wrapper.send_buffer().await?; + output_wrapper + .send_status( + req_id, + StatusCode::SSH_FX_FAILURE, + "error writing ", + ) + .await?; return Err(SftpError::FileServerError(e)); } }; @@ -733,117 +731,14 @@ where // Ok(()) } } - -struct OutputWrapper<'a, 'g> { - sink: SftpSink<'a>, - channel_out: ChanOut<'g>, -} - -impl<'a, 'g> OutputWrapper<'a, 'g> { - pub fn new(buffer: &'a mut [u8], channel_out: ChanOut<'g>) -> Self { - let sink = SftpSink::new(buffer); - OutputWrapper { channel_out, sink } - } - - pub fn reset(&mut self) { - self.sink.reset(); - } - - pub fn get_mut_sink_ref(&mut self) -> &mut SftpSink<'a> { - &mut self.sink - } - - // TODO Are we using this? - pub fn encode(&mut self, data: &T) -> SftpResult<()> - where - T: SSHEncode, - { - data.enc(&mut self.sink)?; - Ok(()) - } - // - - /// Finalizes (Prepends the packet length) and send the data in the - /// buffer by the subsystem channel out - pub async fn send_buffer(&mut self) -> SftpResult { - if self.sink.payload_len() == 0 { - debug!("No data to send in the SFTP sink"); - return Ok(0); - } - self.sink.finalize(); - let buffer = self.sink.used_slice(); - info!("Sending buffer: '{:?}'", buffer); - let written = self.channel_out.write(buffer).await?; - self.sink.reset(); - Ok(written) - } - - /// Send the data in the buffer by the subsystem channel out without - /// prepending the packet length to it. - /// - /// This is useful when an SFTP packet header has already being sent - /// or when the data requires an special treatment - pub async fn send_payload(&mut self) -> SftpResult { - let payload = self.sink.payload_slice(); - info!("Sending payload: '{:?}'", payload); - let written = self.channel_out.write(payload).await?; - self.sink.reset(); - Ok(written) - } - - pub fn finalize(&mut self) -> usize { - self.sink.finalize() - } -} - -#[inline] -fn push_ok(req_id: ReqId, sink: &mut SftpSink<'_>) -> Result<(), WireError> { - let response = SftpPacket::Status( - req_id, - Status { - code: StatusCode::SSH_FX_OK, - message: "".into(), - lang: "en-US".into(), - }, - ); - trace!("Pushing an OK status message: {:?}", response); - response.encode_response(sink)?; - Ok(()) -} - -#[inline] -fn push_unsupported( - req_id: ReqId, - sink: &mut SftpSink<'_>, -) -> Result<(), WireError> { - let response = SftpPacket::Status( - req_id, - Status { - code: StatusCode::SSH_FX_OP_UNSUPPORTED, - message: "Not implemented".into(), - lang: "en-US".into(), - }, - ); - debug!("Pushing a unsupported status message: {:?}", response); - response.encode_response(sink)?; - Ok(()) -} - -#[inline] -fn push_general_failure( - req_id: ReqId, - msg: &'static str, - sink: &mut SftpSink<'_>, -) -> Result<(), WireError> { - let response = SftpPacket::Status( - req_id, - Status { - code: StatusCode::SSH_FX_FAILURE, - message: msg.into(), - lang: "en-US".into(), - }, - ); - debug!("Pushing a general failure status message: {:?}", response); - response.encode_response(sink)?; - Ok(()) +/// Function to create an SFTP source and decode an SFTP packet from it +/// to avoid code duplication +fn create_sftp_source_and_packet( + buffer_in: &[u8], + buffer_in_lower_index_bracket: usize, +) -> (SftpSource<'_>, Result, WireError>) { + let mut source = SftpSource::new(&buffer_in[buffer_in_lower_index_bracket..]); + + let sftp_packet = SftpPacket::decode_request(&mut source); + (source, sftp_packet) } diff --git a/sftp/src/sftphandler/sftpoutputchannelwrapper.rs b/sftp/src/sftphandler/sftpoutputchannelwrapper.rs new file mode 100644 index 00000000..7710b7c3 --- /dev/null +++ b/sftp/src/sftphandler/sftpoutputchannelwrapper.rs @@ -0,0 +1,94 @@ +use crate::error::SftpResult; +use crate::proto::ReqId; +use crate::proto::SftpPacket; +use crate::proto::Status; +use crate::protocol::StatusCode; +use crate::server::SftpSink; + +use sunset::sshwire::WireError; +use sunset_async::ChanOut; + +use embedded_io_async::Write; +#[allow(unused_imports)] +use log::{debug, info, trace, warn}; + +/// Wrapper structure to handle SFTP output operations +/// +/// It wraps an SftpSink and a ChanOut to facilitate sending SFTP packets +/// even when they require multiple iterations +pub struct SftpOutputChannelWrapper<'a, 'g> { + sink: SftpSink<'a>, + channel_out: ChanOut<'g>, +} + +impl<'a, 'g> SftpOutputChannelWrapper<'a, 'g> { + /// Creates a new OutputWrapper + /// + /// This structure wraps an SftpSink and a ChanOut to facilitate + /// sending SFTP packets even when they require multiple steps + pub fn new(buffer: &'a mut [u8], channel_out: ChanOut<'g>) -> Self { + let sink = SftpSink::new(buffer); + SftpOutputChannelWrapper { channel_out, sink } + } + + /// Finalizes (Prepends the packet length) and send the data in the + /// buffer by the subsystem channel out + pub async fn send_buffer(&mut self) -> SftpResult { + if self.sink.payload_len() == 0 { + debug!("No data to send in the SFTP sink"); + return Ok(0); + } + self.sink.finalize(); + let buffer = self.sink.used_slice(); + info!("Sending buffer: '{:?}'", buffer); + let written = self.channel_out.write(buffer).await?; + self.sink.reset(); + Ok(written) + } + + /// Send the data in the buffer by the subsystem channel out without + /// prepending the packet length to it. + /// + /// This is useful when an SFTP packet header has already being sent + /// or when the data requires an special treatment + pub async fn send_payload(&mut self) -> SftpResult { + let payload = self.sink.payload_slice(); + info!("Sending payload: '{:?}'", payload); + let written = self.channel_out.write(payload).await?; + self.sink.reset(); + Ok(written) + } + + /// Push a status message into the channel out + pub async fn send_status( + &mut self, + req_id: ReqId, + status: StatusCode, + msg: &'static str, + ) -> Result<(), WireError> { + let response = SftpPacket::Status( + req_id, + Status { code: status, message: msg.into(), lang: "en-US".into() }, + ); + trace!("Pushing a status message: {:?}", response); + self.send_packet(response); + + Ok(()) + } + + /// Push an SFTP Packet into the channel out + pub async fn send_packet( + &mut self, + packet: SftpPacket<'_>, + ) -> Result<(), WireError> { + packet.encode_response(&mut self.sink)?; + self.send_buffer().await?; + Ok(()) + } + + pub async fn push(&mut self, item: &impl SSHEncode) -> Result<(), WireError> { + item.enc(&mut self.sink)?; + self.send_buffer().await?; + Ok(()) + } +} diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 895605c4..64c9a9ce 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,11 +1,13 @@ -use log::debug; - +use crate::error::SftpResult; +use crate::sftphandler::SftpOutputChannelWrapper; use crate::{ handles::OpaqueFileHandle, proto::{Attrs, Name, ReqId, StatusCode}, }; use core::marker::PhantomData; +use log::debug; + // use futures::executor::block_on; TODO Deal with the async nature of [`ChanOut`] /// Result used to store the result of an Sftp Operation @@ -148,6 +150,12 @@ impl<'g, 'a> ReadReply<'g, 'a> { pub fn reply(self, data: &[u8]) {} } +// TODO Implement correct Channel Out +pub struct ChanOut<'g, 'a> { + _phantom_g: PhantomData<&'g ()>, // 'g look what these might be ChanIO lifetime + _phantom_a: PhantomData<&'a ()>, // a' Why the second lifetime if ChanIO only needs one +} + // TODO Define this /// Dir Reply is the structure that will be "visiting" the [`SftpServer`] /// trait @@ -172,39 +180,47 @@ pub struct DirReply<'g, 'a> { /// Used during the req_id: ReqId, /// To test muting operations - muting: &'a mut u32, - chan: ChanOut<'g, 'a>, + chan_out: &'g mut SftpOutputChannelWrapper<'g>, } impl<'g, 'a> DirReply<'g, 'a> { /// I am faking a DirReply to prototype it - pub fn mock(req_id: ReqId, muting: &'a mut u32) -> Self { - DirReply { - chan: ChanOut { _phantom_g: PhantomData, _phantom_a: PhantomData }, - muting, - req_id, - } + // pub fn mock(req_id: ReqId, muting: &'a mut u32) -> Self { + // DirReply { + // chan_out: ChanOut { _phantom_g: PhantomData, _phantom_a: PhantomData }, + // // muting, + // req_id, + // } + // } + pub fn new( + req_id: ReqId, + chan_out_wrapper: &'g mut SftpOutputChannelWrapper<'g, 'g>, + ) -> Self { + DirReply { chan_out: chan_out_wrapper, req_id } } // TODO this will need to do async execution /// mocks sending an item via a stdio pub fn send_item(&mut self, data: &[u8]) { - *self.muting += 1; - debug!("Muted incremented {:?}. Got data: {:?}", self.muting, data); + // *self.muting += 1; + debug!("Send item: data = {:?}", data); } // TODO this will need to do async execution /// Must be call it first. Make this enforceable - pub fn send_header(&self, get_count: u32, get_encoded_len: u32) { + pub async fn send_header( + &mut self, + get_count: u32, + get_encoded_len: u32, + ) -> SftpResult<()> { debug!( "I will send the header here for request id {:?}: count = {:?}, length = {:?}", self.req_id, get_count, get_encoded_len ); + self.chan_out.push(&get_encoded_len).await?; + self.chan_out.push(&(104 as u8)).await?; + self.chan_out.push(&get_count).await?; + self.chan_out.send_payload().await?; + Ok(()) } } - -// TODO Implement correct Channel Out -pub struct ChanOut<'g, 'a> { - _phantom_g: PhantomData<&'g ()>, // 'g look what these might be ChanIO lifetime - _phantom_a: PhantomData<&'a ()>, // a' Why the second lifetime if ChanIO only needs one -} From 335cd4cfe341581caf79e4fb7512e8fe28e04235 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 5 Nov 2025 11:34:47 +1100 Subject: [PATCH 324/393] [skip ci] WIP: Adding Producer and Consumer to avoid mutable references writing to ChanOut [skip ci] WIP: Removing DirReplay wrong lifetime parameters --- demo/sftp/std/src/demosftpserver.rs | 2 +- demo/sftp/std/src/main.rs | 15 +- sftp/Cargo.toml | 2 + sftp/src/sftphandler/mod.rs | 1 + .../sftphandler/sftpoutputchannelhandler.rs | 162 ++++++++++++++++++ .../sftphandler/sftpoutputchannelwrapper.rs | 15 +- 6 files changed, 182 insertions(+), 15 deletions(-) create mode 100644 sftp/src/sftphandler/sftpoutputchannelhandler.rs diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 5c6818ed..8264ce4f 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -277,7 +277,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { fn readdir( &mut self, opaque_dir_handle: &DemoOpaqueFileHandle, - visitor: &mut DirReply<'_, '_>, + visitor: &mut DirReply<'_>, ) -> SftpOpResult<()> { debug!("read dir for {:?}", opaque_dir_handle); diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 4052cbb8..23c752de 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -157,15 +157,12 @@ impl DemoServer for StdDemo { "./demo/sftp/std/testing/out/".to_string(), ); - let mut sftp_handler = - SftpHandler::::new( - &mut file_server, - &mut incomplete_request_buffer, - ); - - sftp_handler - .process_loop(stdio, &mut buffer_in, &mut buffer_out) - .await?; + SftpHandler::::new( + &mut file_server, + &mut incomplete_request_buffer, + ) + .process_loop(stdio, &mut buffer_in, &mut buffer_out) + .await?; Ok::<_, Error>(()) } { diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 9a591f9f..86fb6d52 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -13,3 +13,5 @@ embedded-io-async = "0.6" num_enum = {version = "0.7.4"} paste = "1.0" log = "0.4" +embassy-sync = "0.7.2" +embassy-futures = "0.1.2" diff --git a/sftp/src/sftphandler/mod.rs b/sftp/src/sftphandler/mod.rs index fc32ff97..59cbb825 100644 --- a/sftp/src/sftphandler/mod.rs +++ b/sftp/src/sftphandler/mod.rs @@ -1,5 +1,6 @@ mod partialwriterequesttracker; mod sftphandler; +mod sftpoutputchannelhandler; mod sftpoutputchannelwrapper; pub use partialwriterequesttracker::PartialWriteRequestTracker; diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs new file mode 100644 index 00000000..e0fda789 --- /dev/null +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -0,0 +1,162 @@ +#![no_std] +use crate::error::SftpResult; +use crate::proto::{ReqId, SftpPacket, Status, StatusCode}; +use crate::server::SftpSink; + +use sunset_async::ChanOut; + +use embassy_sync::blocking_mutex::raw::RawMutex; +use embassy_sync::pipe::{Pipe, Reader as PipeReader, Writer as PipeWriter}; +use embedded_io_async::Write; + +use log::{debug, trace}; + +//// This is the beginning of a new idea: +/// I want to pass ref of an item where different methods in the sftphandler can +/// send data down the ChanOut. That would mutate the ChanOut (since it mutates on write) +/// and would violate the basic rule of only one mut borrow. +/// +/// To overcome this hurdle, I can use a two part solution related with a channel or a pipe. +/// +/// +/// Some notes: +/// +/// # sftpoutputchannelwrapper +/// Currently is a mutable entity. That causes issues since it needs to be mutated during loops. +/// ## first usage: +/// push SFTPEncode n times (composition) +// send_payload() +/// ## Second usage: +/// send_packet(SftpPacket) +/// ## last usage: +/// send_status('static str for messages) -> calls send_packet +/// # Alternative to avoid mutation: a channel or pipe +/// What would we put in the pipe? +/// ## 1st. composition: +/// We would create an SftpSink, add the SFTPEncode items and send it as a buffer. Maybe Len field eq to 0? +/// Maybe receive an SftpSink? +/// ## 2nd. SftpPacket +/// SftpSink, encode a packet, terminate it and send the SftpSink. len != 0 +/// ## 3rd. SftpPacket::Status +/// Compose the Status SftpPacket, Encode it in SftpSink, finalise it, send the buffer +// + +// enum AgentMsg { message to be sent} + +// static' RAW_PIPE = Pipe::::new(); + +pub struct SftpOutputPipe { + pipe: Pipe, + capacity: usize, +} + +/// M: SunsetRawMutex +impl SftpOutputPipe { + /// Creates an empty SftpOutputPipe. + /// The output channel will be consumed during the split call + /// + /// Usage: + /// + /// let output_pipe = SftpOutputPipe::::new(); + /// + fn new() -> Self { + SftpOutputPipe { pipe: Pipe::new(), capacity: N } + } + + /// Returns the inner pipe capacity. This method can be called after + /// split. + fn get_capacity(&self) -> usize { + self.capacity + } + + // TODO: Check if it panics when called twice + // TODO: Fix Doc links + /// Get a Consumer and Producer pair so the producer can send data to the + /// output channel without mutable borrows. + /// + /// The ['SftpOutputConsumer'] needs to be running to write data to the + /// ['ChanOut'] + /// + /// ## Lifetimes + /// The lifetime indicates that the lifetime of self, ChanOut and the + /// consumer and producer are the same. I chose this because if the ChanOut + /// is closed, there is no point on having a pipe outliving it. + fn split<'a>( + &'a mut self, + ssh_chan_out: ChanOut<'a>, + ) -> (SftpOutputConsumer<'a, M, N>, SftpOutputProducer<'a, M, N>) { + let (reader, writer) = self.pipe.split(); + (SftpOutputConsumer { reader, ssh_chan_out }, SftpOutputProducer { writer }) + } +} +pub struct SftpOutputConsumer<'a, M, const N: usize> +where + M: RawMutex, +{ + reader: PipeReader<'a, M, N>, + ssh_chan_out: ChanOut<'a>, +} +impl<'a, M, const N: usize> SftpOutputConsumer<'a, M, N> +where + M: RawMutex, +{ + /// Run it to start the piping + pub async fn receive_task(&mut self) -> SftpResult<()> { + let mut buf = [0u8; N]; + loop { + let rl = self.reader.read(&mut buf).await; + debug!("Read {} bytes", rl); + if rl > 0 { + self.ssh_chan_out.write_all(&buf[..rl]).await?; + } + } + } +} + +#[derive(Clone)] +pub struct SftpOutputProducer<'a, M, const N: usize> +where + M: RawMutex, +{ + writer: PipeWriter<'a, M, N>, +} +impl<'a, M, const N: usize> SftpOutputProducer<'a, M, N> +where + M: RawMutex, +{ + pub async fn send_payload(&self, sftp_sink: &SftpSink<'_>) -> SftpResult<()> { + let buf = sftp_sink.payload_slice(); + Self::send_buffer(&self.writer, &buf).await; + Ok(()) + } + + pub async fn send_status( + &self, + req_id: ReqId, + status: StatusCode, + msg: &'static str, + ) -> SftpResult<()> { + let response = SftpPacket::Status( + req_id, + Status { code: status, message: msg.into(), lang: "en-US".into() }, + ); + debug!("Pushing a status message: {:?}", response); + self.send_packet(&response).await?; + Ok(()) + } + + /// Push an SFTP Packet into the channel out + pub async fn send_packet(&self, packet: &SftpPacket<'_>) -> SftpResult<()> { + let mut buf = [0u8; N]; + let mut sink = SftpSink::new(&mut buf); + packet.encode_response(&mut sink); + debug!("Sending packet {:?}", packet); + Self::send_buffer(&self.writer, &buf).await; + Ok(()) + } + + async fn send_buffer(writer: &PipeWriter<'a, M, N>, buf: &[u8]) { + trace!("Sending buffer {:?}", buf); + writer.write(buf).await; + } +} diff --git a/sftp/src/sftphandler/sftpoutputchannelwrapper.rs b/sftp/src/sftphandler/sftpoutputchannelwrapper.rs index 7710b7c3..e9f7033f 100644 --- a/sftp/src/sftphandler/sftpoutputchannelwrapper.rs +++ b/sftp/src/sftphandler/sftpoutputchannelwrapper.rs @@ -5,6 +5,7 @@ use crate::proto::Status; use crate::protocol::StatusCode; use crate::server::SftpSink; +use sunset::sshwire::SSHEncode; use sunset::sshwire::WireError; use sunset_async::ChanOut; @@ -12,21 +13,25 @@ use embedded_io_async::Write; #[allow(unused_imports)] use log::{debug, info, trace, warn}; +pub struct BufferMsg { + pub data: [u8; N], + pub used: usize, +} /// Wrapper structure to handle SFTP output operations /// /// It wraps an SftpSink and a ChanOut to facilitate sending SFTP packets /// even when they require multiple iterations -pub struct SftpOutputChannelWrapper<'a, 'g> { - sink: SftpSink<'a>, +pub struct SftpOutputChannelWrapper<'g> { + sink: SftpSink<'g>, channel_out: ChanOut<'g>, } -impl<'a, 'g> SftpOutputChannelWrapper<'a, 'g> { +impl<'g> SftpOutputChannelWrapper<'g> { /// Creates a new OutputWrapper /// /// This structure wraps an SftpSink and a ChanOut to facilitate /// sending SFTP packets even when they require multiple steps - pub fn new(buffer: &'a mut [u8], channel_out: ChanOut<'g>) -> Self { + pub fn new(buffer: &'g mut [u8], channel_out: ChanOut<'g>) -> Self { let sink = SftpSink::new(buffer); SftpOutputChannelWrapper { channel_out, sink } } @@ -48,7 +53,7 @@ impl<'a, 'g> SftpOutputChannelWrapper<'a, 'g> { /// Send the data in the buffer by the subsystem channel out without /// prepending the packet length to it. - /// + /// /// This is useful when an SFTP packet header has already being sent /// or when the data requires an special treatment pub async fn send_payload(&mut self) -> SftpResult { From 8c087eb6f0a9fbaea45411485714bf9a60264973 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 5 Nov 2025 15:00:38 +1100 Subject: [PATCH 325/393] [skip ci] WIP: Improved SftpSource readability --- sftp/src/sftpsource.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 7e1918e3..1a29a149 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -60,7 +60,12 @@ impl<'de> SftpSource<'de> { /// **Warning**: will only work in well formed packets, in other case /// the result will contain garbage pub(crate) fn peak_packet_type(&self) -> WireResult { - if self.buffer.len() < SFTP_FIELD_ID_INDEX + 1 { + if self.buffer.len() <= SFTP_FIELD_ID_INDEX { + error!( + "Peak packet type failed: buffer len <= SFTP_FIELD_ID_INDEX ( {:?} <= {:?})", + self.buffer.len(), + SFTP_FIELD_ID_INDEX + ); Err(WireError::RanOut) } else { Ok(SftpNum::from(self.buffer[SFTP_FIELD_ID_INDEX])) @@ -88,9 +93,15 @@ impl<'de> SftpSource<'de> { } /// Assuming that the buffer contains a [`proto::Write`] request packet initial - /// bytes and not its totality, extracts a partial version of the write request - /// and a Write request tracker to handle and a tracker to continue processing - /// subsequent portions of the request from a SftpSource + /// bytes and not its totality: + /// + /// **Returns**: + /// + /// - An [`OpaqueFileHandle`] to guide the Write operation, + /// - Request ID as [`ReqId`], + /// - Offset as [`u64`] + /// - Data in the buffer as [`BinString`] + /// - [`PartialWriteRequestTracker`] to handle subsequent portions of the request /// /// **Warning**: will only work in well formed write packets, in other case /// the result will contain garbage From 158998a0edcf9e5a2c55ca0077ac20d59282e44f Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 5 Nov 2025 15:01:19 +1100 Subject: [PATCH 326/393] [skip ci] WIP: Removing excessive lifetimes to build successfully --- sftp/src/sftpserver.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 64c9a9ce..9e323ad6 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -78,7 +78,7 @@ where fn readdir( &mut self, opaque_dir_handle: &T, - reply: &mut DirReply<'_, '_>, + reply: &mut DirReply<'_>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", @@ -176,14 +176,14 @@ pub struct ChanOut<'g, 'a> { /// c. Send each serialized `NameEntry`, excluding their length using /// the `send_item` method /// -pub struct DirReply<'g, 'a> { +pub struct DirReply<'g> { /// Used during the req_id: ReqId, /// To test muting operations chan_out: &'g mut SftpOutputChannelWrapper<'g>, } -impl<'g, 'a> DirReply<'g, 'a> { +impl<'g> DirReply<'g> { /// I am faking a DirReply to prototype it // pub fn mock(req_id: ReqId, muting: &'a mut u32) -> Self { // DirReply { @@ -194,7 +194,7 @@ impl<'g, 'a> DirReply<'g, 'a> { // } pub fn new( req_id: ReqId, - chan_out_wrapper: &'g mut SftpOutputChannelWrapper<'g, 'g>, + chan_out_wrapper: &'g mut SftpOutputChannelWrapper<'g>, ) -> Self { DirReply { chan_out: chan_out_wrapper, req_id } } From af9bd6596fe21b2786b369bee4a96280c5a22aef Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 5 Nov 2025 15:02:22 +1100 Subject: [PATCH 327/393] [skip ci] WIP: Improved readability in requestholder.rs [skip ci] WIP: Fixing the Pipe mutex to main crate SunsetRawMutex, solve send_packet bug and added debugging --- sftp/src/requestholder.rs | 40 ++++++------ .../sftphandler/sftpoutputchannelhandler.rs | 65 ++++++++----------- 2 files changed, 49 insertions(+), 56 deletions(-) diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs index e1427fc2..81dc9411 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/requestholder.rs @@ -108,7 +108,7 @@ impl<'a> RequestHolder<'a> { } /// Using the content of the `RequestHolder` tries to find a valid - /// SFTP request appending from slice into the internal buffer to + /// SFTP request appending bytes from slice_in into the internal buffer to /// form a valid request. /// /// Reset and increase the `appended()` counter. @@ -126,7 +126,7 @@ impl<'a> RequestHolder<'a> { /// - `Err(Bug)`: An unexpected condition arises pub fn try_append_for_valid_request( &mut self, - slice: &[u8], + slice_in: &[u8], ) -> RequestHolderResult<()> { debug!( "try_append_for_valid_request: self = {:?}\n\ @@ -134,7 +134,7 @@ impl<'a> RequestHolder<'a> { Length of slice to append from = {:?}", self, self.remaining_len(), - slice.len() + slice_in.len() ); if !self.busy { @@ -150,13 +150,13 @@ impl<'a> RequestHolder<'a> { self.appended = 0; // reset appended bytes counter // If we will not be able to read the SFTP packet ID we clearly need more data - if self.buffer_fill_index + slice.len() < proto::SFTP_FIELD_ID_INDEX { - self.try_append_slice(&slice)?; + if self.buffer_fill_index + slice_in.len() < proto::SFTP_FIELD_ID_INDEX { + self.try_append_slice(&slice_in)?; error!( "[Buffer fill index = {:?}] + [slice.len = {:?}] = {:?} < SFTP field id index = {:?}", self.buffer_fill_index, - slice.len(), - self.buffer_fill_index + slice.len(), + slice_in.len(), + self.buffer_fill_index + slice_in.len(), proto::SFTP_FIELD_ID_INDEX ); return Err(RequestHolderError::RanOut); @@ -175,15 +175,15 @@ impl<'a> RequestHolder<'a> { complete_to_id_index, proto::SFTP_FIELD_ID_INDEX ); - if complete_to_id_index > slice.len() { - self.try_append_slice(&slice)?; + if complete_to_id_index > slice_in.len() { + self.try_append_slice(&slice_in)?; error!( "The slice to include to the held fragment is too \ short to complete to id index. More data is required." ); return Err(RequestHolderError::RanOut); } else { - self.try_append_slice(&slice[..complete_to_id_index])?; + self.try_append_slice(&slice_in[..complete_to_id_index])?; }; } @@ -213,14 +213,16 @@ impl<'a> RequestHolder<'a> { + remaining_packet_len ); if remaining_packet_len <= self.remaining_len() { - // We have all the remaining packet bytes in the slice and fits in the buffer + // The remaining bytes would fill in the buffer - if (slice.len()) < (remaining_packet_len + self.appended()) { - self.try_append_slice(&slice[self.appended()..])?; + if (slice_in.len()) < (remaining_packet_len + self.appended()) { + // the slice_in does not contain all the remaining bytes + // We added them an request more + self.try_append_slice(&slice_in[self.appended()..])?; return Err(RequestHolderError::RanOut); } else { self.try_append_slice( - &slice[self.appended()..remaining_packet_len], + &slice_in[self.appended()..remaining_packet_len], )?; return Ok(()); } @@ -229,22 +231,22 @@ impl<'a> RequestHolder<'a> { // But they may not fit in the slice neither let start = self.appended(); - let end = self.remaining_len().min(slice.len() - self.appended()); + let end = self.remaining_len().min(slice_in.len() - self.appended()); debug!( "Will finally take the range: [{:?}..{:?}] from the slice [0..{:?}]", start, end, - slice.len() + slice_in.len() ); self.try_append_slice( - &slice[self.appended() - ..self.remaining_len().min(slice.len() - self.appended())], + &slice_in[self.appended() + ..self.remaining_len().min(slice_in.len() - self.appended())], )?; if self.is_full() { return Err(RequestHolderError::NoRoom); } else { - return Err(RequestHolderError::RanOut); + return Err(RequestHolderError::RanOut); // More bytes are needed to complete the Write request } } } diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index e0fda789..4d456b84 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -1,13 +1,12 @@ -#![no_std] use crate::error::SftpResult; use crate::proto::{ReqId, SftpPacket, Status, StatusCode}; use crate::server::SftpSink; use sunset_async::ChanOut; -use embassy_sync::blocking_mutex::raw::RawMutex; use embassy_sync::pipe::{Pipe, Reader as PipeReader, Writer as PipeWriter}; use embedded_io_async::Write; +use sunset_async::SunsetRawMutex; use log::{debug, trace}; @@ -43,29 +42,29 @@ use log::{debug, trace}; // enum AgentMsg { message to be sent} -// static' RAW_PIPE = Pipe::::new(); +// static' RAW_PIPE = Pipe::::new(); -pub struct SftpOutputPipe { - pipe: Pipe, +pub struct SftpOutputPipe { + pipe: Pipe, capacity: usize, } -/// M: SunsetRawMutex -impl SftpOutputPipe { +/// M: SunsetSunsetRawMutex +impl SftpOutputPipe { /// Creates an empty SftpOutputPipe. /// The output channel will be consumed during the split call /// /// Usage: /// - /// let output_pipe = SftpOutputPipe::::new(); + /// let output_pipe = SftpOutputPipe::::new(); /// - fn new() -> Self { + pub fn new() -> Self { SftpOutputPipe { pipe: Pipe::new(), capacity: N } } /// Returns the inner pipe capacity. This method can be called after /// split. - fn get_capacity(&self) -> usize { + pub fn get_capacity(&self) -> usize { self.capacity } @@ -81,31 +80,27 @@ impl SftpOutputPipe { /// The lifetime indicates that the lifetime of self, ChanOut and the /// consumer and producer are the same. I chose this because if the ChanOut /// is closed, there is no point on having a pipe outliving it. - fn split<'a>( + pub fn split<'a>( &'a mut self, ssh_chan_out: ChanOut<'a>, - ) -> (SftpOutputConsumer<'a, M, N>, SftpOutputProducer<'a, M, N>) { + ) -> (SftpOutputConsumer<'a, N>, SftpOutputProducer<'a, N>) { let (reader, writer) = self.pipe.split(); (SftpOutputConsumer { reader, ssh_chan_out }, SftpOutputProducer { writer }) } } -pub struct SftpOutputConsumer<'a, M, const N: usize> -where - M: RawMutex, -{ - reader: PipeReader<'a, M, N>, +pub struct SftpOutputConsumer<'a, const N: usize> { + reader: PipeReader<'a, SunsetRawMutex, N>, ssh_chan_out: ChanOut<'a>, } -impl<'a, M, const N: usize> SftpOutputConsumer<'a, M, N> -where - M: RawMutex, -{ +impl<'a, const N: usize> SftpOutputConsumer<'a, N> { /// Run it to start the piping pub async fn receive_task(&mut self) -> SftpResult<()> { + debug!("Running SftpOutout Consumer Reader task"); let mut buf = [0u8; N]; loop { let rl = self.reader.read(&mut buf).await; - debug!("Read {} bytes", rl); + debug!("Output Consumer Reader task: Reads {} bytes", rl); + debug!("Output Consumer Reader task: Bytes {:?}", &buf[..rl]); if rl > 0 { self.ssh_chan_out.write_all(&buf[..rl]).await?; } @@ -114,16 +109,10 @@ where } #[derive(Clone)] -pub struct SftpOutputProducer<'a, M, const N: usize> -where - M: RawMutex, -{ - writer: PipeWriter<'a, M, N>, +pub struct SftpOutputProducer<'a, const N: usize> { + writer: PipeWriter<'a, SunsetRawMutex, N>, } -impl<'a, M, const N: usize> SftpOutputProducer<'a, M, N> -where - M: RawMutex, -{ +impl<'a, const N: usize> SftpOutputProducer<'a, N> { pub async fn send_payload(&self, sftp_sink: &SftpSink<'_>) -> SftpResult<()> { let buf = sftp_sink.payload_slice(); Self::send_buffer(&self.writer, &buf).await; @@ -140,7 +129,7 @@ where req_id, Status { code: status, message: msg.into(), lang: "en-US".into() }, ); - debug!("Pushing a status message: {:?}", response); + debug!("Output Producer: Pushing a status message: {:?}", response); self.send_packet(&response).await?; Ok(()) } @@ -149,14 +138,16 @@ where pub async fn send_packet(&self, packet: &SftpPacket<'_>) -> SftpResult<()> { let mut buf = [0u8; N]; let mut sink = SftpSink::new(&mut buf); - packet.encode_response(&mut sink); - debug!("Sending packet {:?}", packet); - Self::send_buffer(&self.writer, &buf).await; + packet.encode_response(&mut sink)?; + debug!("Output Producer: Sending packet {:?}", packet); + sink.finalize(); + Self::send_buffer(&self.writer, &sink.used_slice()).await; Ok(()) } - async fn send_buffer(writer: &PipeWriter<'a, M, N>, buf: &[u8]) { - trace!("Sending buffer {:?}", buf); + async fn send_buffer(writer: &PipeWriter<'a, SunsetRawMutex, N>, buf: &[u8]) { + debug!("Output Producer: Sends {:?} bytes", buf.len()); + trace!("Output Producer: Sending buffer {:?}", buf); writer.write(buf).await; } } From d837a8e030bb0bc8f456ee9fab78e9cdb37a8ae3 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 5 Nov 2025 15:06:42 +1100 Subject: [PATCH 328/393] [skip ci] WIP: Fixing Bug where an error was thrown instead of trying to encode a packet --- sftp/src/sftphandler/sftphandler.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 71969f4b..7fafec0c 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -204,13 +204,11 @@ where continue; } RequestHolderError::NoRoom => { - error!( - "The request holder if full but the request in incomplete. \ - Consider increasing its size" - ); - // TODO react to this situation with an internal server error - return Err(SunsetError::NoRoom {}.into()); - } + warn!( + "The request holder is full but the request in is incomplete. \ + We will try to decode it" + ); + } _ => { error!( From 97b6f8ef4aeea8e7d054f257fba25cf6681042fc Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 5 Nov 2025 15:10:31 +1100 Subject: [PATCH 329/393] [skip ci] WIP: Refactor sftphandler.rs and main.rs to accommodate the new Producer-Consumer for ChanOut It fixes the mutable borrow problems but I am getting the client disconnected with writing request: ``` Received message too long 1572864 Ensure the remote shell produces no output for non-interactive sessions. ``` [skip ci] WIP: Looking for the error. Made some small readability changes [skip ci] WIP: looking for traces explaining where the length [0,0,0,18] becames [18,0,0,0] Increasing verbosity level globally Still I cannot find where it is happening if it happens in Sunset SSH [skip ci] WIP: Tyding up: Removing deprecated parts of code ,commenting out others and adding comments --- demo/sftp/std/src/main.rs | 19 +- .../std/testing/test_long_write_requests.sh | 8 +- sftp/src/sftphandler/mod.rs | 2 - sftp/src/sftphandler/sftphandler.rs | 475 +++++++++--------- .../sftphandler/sftpoutputchannelhandler.rs | 36 +- .../sftphandler/sftpoutputchannelwrapper.rs | 99 ---- sftp/src/sftpserver.rs | 28 +- 7 files changed, 290 insertions(+), 377 deletions(-) delete mode 100644 sftp/src/sftphandler/sftpoutputchannelwrapper.rs diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 23c752de..ac251e37 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -89,7 +89,7 @@ impl DemoServer for StdDemo { async fn run(&self, serv: &SSHServer<'_>, mut common: DemoCommon) -> Result<()> { let chan_pipe = Channel::::new(); - let prog_loop_inner = async { + let ssh_loop_inner = async { loop { let mut ph = ProgressHolder::new(); let ev = serv.progress(&mut ph).await?; @@ -118,7 +118,7 @@ impl DemoServer for StdDemo { warn!( "request for subsystem '{}' not implemented: fail", a.command()? - ); + ); a.fail()?; } } @@ -130,9 +130,9 @@ impl DemoServer for StdDemo { Ok::<_, Error>(()) }; - let prog_loop = async { + let ssh_loop = async { info!("prog_loop started"); - if let Err(e) = prog_loop_inner.await { + if let Err(e) = ssh_loop_inner.await { warn!("Prog Loop Exited: {e:?}"); return Err(e); } @@ -148,8 +148,7 @@ impl DemoServer for StdDemo { // TODO Do some research to find reasonable default buffer lengths let mut buffer_in = [0u8; 512]; - let mut buffer_out = [0u8; 384]; - let mut incomplete_request_buffer = [0u8; 128]; + let mut incomplete_request_buffer = [0u8; 256]; match { let stdio = serv.stdio(ch).await?; @@ -157,11 +156,11 @@ impl DemoServer for StdDemo { "./demo/sftp/std/testing/out/".to_string(), ); - SftpHandler::::new( + SftpHandler::::new( &mut file_server, &mut incomplete_request_buffer, ) - .process_loop(stdio, &mut buffer_in, &mut buffer_out) + .process_loop(stdio, &mut buffer_in) .await?; Ok::<_, Error>(()) @@ -179,7 +178,7 @@ impl DemoServer for StdDemo { Ok::<_, Error>(()) }; - let selected = select(prog_loop, sftp_loop).await; + let selected = select(ssh_loop, sftp_loop).await; match selected { embassy_futures::select::Either::First(res) => { warn!("prog_loop finished: {:?}", res); @@ -206,7 +205,7 @@ async fn listen( #[embassy_executor::main] async fn main(spawner: Spawner) { env_logger::builder() - .filter_level(log::LevelFilter::Debug) + .filter_level(log::LevelFilter::Trace) .filter_module("sunset::runner", log::LevelFilter::Info) .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) diff --git a/demo/sftp/std/testing/test_long_write_requests.sh b/demo/sftp/std/testing/test_long_write_requests.sh index a0104e17..532edd32 100755 --- a/demo/sftp/std/testing/test_long_write_requests.sh +++ b/demo/sftp/std/testing/test_long_write_requests.sh @@ -5,17 +5,17 @@ REMOTE_HOST="192.168.69.2" REMOTE_USER="any" # Define test files -FILES=("100MB_random" "1024MB_random") +FILES=("100MB_random") # Generate random data files echo "Generating random data files..." dd if=/dev/random bs=1048576 count=100 of=./100MB_random 2>/dev/null -dd if=/dev/random bs=1048576 count=1024 of=./1024MB_random 2>/dev/null +# dd if=/dev/random bs=1048576 count=1024 of=./1024MB_random 2>/dev/null echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." # Upload all files -sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} -vvv << EOF $(printf 'put ./%s\n' "${FILES[@]}") bye EOF @@ -33,6 +33,6 @@ for file in "${FILES[@]}"; do done echo "Cleaning up local files..." -rm -f ./*_random ./out/*_random +rm -f -r ./*_random ./out/*_random echo "Upload test completed." \ No newline at end of file diff --git a/sftp/src/sftphandler/mod.rs b/sftp/src/sftphandler/mod.rs index 59cbb825..c7660cd1 100644 --- a/sftp/src/sftphandler/mod.rs +++ b/sftp/src/sftphandler/mod.rs @@ -1,8 +1,6 @@ mod partialwriterequesttracker; mod sftphandler; mod sftpoutputchannelhandler; -mod sftpoutputchannelwrapper; pub use partialwriterequesttracker::PartialWriteRequestTracker; pub use sftphandler::SftpHandler; -pub use sftpoutputchannelwrapper::SftpOutputChannelWrapper; diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 7fafec0c..37bd0a23 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -7,12 +7,14 @@ use crate::proto::{ SftpPacket, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; -use crate::server::DirReply; use crate::sftperror::SftpResult; -use crate::sftphandler::sftpoutputchannelwrapper::SftpOutputChannelWrapper; +use crate::sftphandler::sftpoutputchannelhandler::{ + SftpOutputPipe, SftpOutputProducer, +}; use crate::sftpserver::SftpServer; use crate::sftpsource::SftpSource; +use embassy_futures::select::select; use sunset::Error as SunsetError; use sunset::sshwire::{SSHSource, WireError}; use sunset_async::ChanInOut; @@ -88,7 +90,7 @@ enum FragmentedRequestState { /// It will delegate request to an [`crate::sftpserver::SftpServer`] /// implemented by the library /// user taking into account the local system details. -pub struct SftpHandler<'a, T, S> +pub struct SftpHandler<'a, T, S, const BUFFER_OUT_SIZE: usize> where T: OpaqueFileHandle, S: SftpServer<'a, T>, @@ -107,7 +109,7 @@ where incomplete_request_holder: RequestHolder<'a>, } -impl<'a, T, S> SftpHandler<'a, T, S> +impl<'a, T, S, const BUFFER_OUT_SIZE: usize> SftpHandler<'a, T, S, BUFFER_OUT_SIZE> where T: OpaqueFileHandle, S: SftpServer<'a, T>, @@ -143,10 +145,10 @@ where /// /// **Returns**: A result containing the number of bytes used in /// `buffer_out` - async fn process<'g>( + async fn process( &mut self, buffer_in: &[u8], - output_wrapper: &mut SftpOutputChannelWrapper<'a, 'g>, + output_producer: &SftpOutputProducer<'_, BUFFER_OUT_SIZE>, ) -> SftpResult<()> { let in_len = buffer_in.len(); let mut buffer_in_lower_index_bracket = 0; @@ -171,203 +173,191 @@ where match &self.state { // There is a fragmented request in process of processing - SftpHandleState::Fragmented(fragment_case) => { - match fragment_case { - FragmentedRequestState::ProcessingClippedRequest => { - if let Err(e) = self - .incomplete_request_holder - .try_append_for_valid_request( - &buffer_in[buffer_in_lower_index_bracket..], - ) - { - match e { - RequestHolderError::RanOut => { - warn!( - "There was not enough bytes in the buffer_in. \ - We will continue adding bytes" - ); - buffer_in_lower_index_bracket += self - .incomplete_request_holder - .appended(); - continue; - } - RequestHolderError::WireError( - WireError::RanOut, - ) => { - warn!( - "WIRE ERROR: There was not enough bytes in the buffer_in. \ - We will continue adding bytes" - ); - buffer_in_lower_index_bracket += self - .incomplete_request_holder - .appended(); - continue; - } - RequestHolderError::NoRoom => { + SftpHandleState::Fragmented(fragment_case) => match fragment_case { + FragmentedRequestState::ProcessingClippedRequest => { + if let Err(e) = self + .incomplete_request_holder + .try_append_for_valid_request( + &buffer_in[buffer_in_lower_index_bracket..], + ) + { + match e { + RequestHolderError::RanOut => { + warn!( + "There was not enough bytes in the buffer_in. \ + We will continue adding bytes" + ); + buffer_in_lower_index_bracket += + self.incomplete_request_holder.appended(); + continue; + } + RequestHolderError::WireError(WireError::RanOut) => { + warn!( + "WIRE ERROR: There was not enough bytes in the buffer_in. \ + We will continue adding bytes" + ); + buffer_in_lower_index_bracket += + self.incomplete_request_holder.appended(); + continue; + } + RequestHolderError::NoRoom => { warn!( "The request holder is full but the request in is incomplete. \ We will try to decode it" ); } - _ => { - error!( - "Unhandled error completing incomplete request {:?}", - e, - ); - return Err(SunsetError::Bug.into()); - } + _ => { + error!( + "Unhandled error completing incomplete request {:?}", + e, + ); + return Err(SunsetError::Bug.into()); } - } else { - debug!( - "Incomplete request holder completed the request!" - ); } + } else { + debug!( + "Incomplete request holder completed the request!" + ); + } - let used = self.incomplete_request_holder.appended(); - buffer_in_lower_index_bracket += used; + let used = self.incomplete_request_holder.appended(); + buffer_in_lower_index_bracket += used; - let mut source = SftpSource::new( - &self.incomplete_request_holder.try_get_ref()?, - ); - trace!("Internal Source Content: {:?}", source); - - match SftpPacket::decode_request(&mut source) { - Ok(request) => { - Self::handle_general_request( - &mut self.file_server, - output_wrapper, - request, - ) - .await?; - self.incomplete_request_holder.reset(); - self.state = SftpHandleState::Idle; - } - Err(e) => match e { - WireError::RanOut => match Self::handle_ran_out( - &mut self.file_server, - output_wrapper, - &mut source, - ) - .await - { - Ok(holder) => { - self.partial_write_request_tracker = - Some(holder); - self.incomplete_request_holder.reset(); - self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); + let mut source = SftpSource::new( + &self.incomplete_request_holder.try_get_ref()?, + ); + trace!("Internal Source Content: {:?}", source); + + match SftpPacket::decode_request(&mut source) { + Ok(request) => { + Self::handle_general_request( + &mut self.file_server, + output_producer, + request, + ) + .await?; + self.incomplete_request_holder.reset(); + self.state = SftpHandleState::Idle; + } + Err(e) => match e { + WireError::RanOut => match Self::handle_ran_out( + &mut self.file_server, + output_producer, + &mut source, + ) + .await + { + Ok(holder) => { + self.partial_write_request_tracker = + Some(holder); + self.incomplete_request_holder.reset(); + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); + } + Err(e) => match e { + _ => { + error!( + "handle_ran_out finished with error: {:?}", + e + ); + return Err(SunsetError::Bug.into()); } - Err(e) => match e { - _ => { - error!( - "handle_ran_out finished with error: {:?}", - e - ); - return Err(SunsetError::Bug.into()); - } - }, }, - WireError::NoRoom => { - error!("Not enough space to fit the request") - } - _ => { - error!( - "Unhandled error decoding assembled packet: {:?}", - e - ); - return Err(WireError::PacketWrong.into()); - } }, - } + WireError::NoRoom => { + error!("Not enough space to fit the request") + } + _ => { + error!( + "Unhandled error decoding assembled packet: {:?}", + e + ); + return Err(WireError::PacketWrong.into()); + } + }, } - FragmentedRequestState::ProcessingLongRequest => { - let mut source = SftpSource::new( - &buffer_in[buffer_in_lower_index_bracket..], + } + FragmentedRequestState::ProcessingLongRequest => { + let mut source = SftpSource::new( + &buffer_in[buffer_in_lower_index_bracket..], + ); + trace!("Source content: {:?}", source); + + let mut write_tracker = if let Some(wt) = + self.partial_write_request_tracker.take() + { + wt + } else { + error!( + "BUG: FragmentedRequestState::ProcessingLongRequest cannot take the write tracker" ); - trace!("Source content: {:?}", source); - - let mut write_tracker = if let Some(wt) = - self.partial_write_request_tracker.take() - { - wt - } else { - error!( - "BUG: FragmentedRequestState::ProcessingLongRequest cannot take the write tracker" - ); - return Err(SunsetError::Bug.into()); - }; + return Err(SunsetError::Bug.into()); + }; - let opaque_handle = - write_tracker.get_opaque_file_handle(); + let opaque_handle = write_tracker.get_opaque_file_handle(); - let usable_data = source - .remaining() - .min(write_tracker.get_remain_data_len() as usize); + let usable_data = source + .remaining() + .min(write_tracker.get_remain_data_len() as usize); - let data_segment = // Fails!! - source.dec_as_binstring(usable_data)?; + let data_segment = source.dec_as_binstring(usable_data)?; - let data_segment_len = u32::try_from( - data_segment.0.len(), - ) + let data_segment_len = u32::try_from(data_segment.0.len()) .map_err(|e| { - error!("Error casting data segment len to u32: {e}"); - SunsetError::Bug - })?; - let current_write_offset = - write_tracker.get_remain_data_offset(); - write_tracker.update_remaining_after_partial_write( - data_segment_len, - ); + error!("Error casting data segment len to u32: {e}"); + SunsetError::Bug + })?; + let current_write_offset = + write_tracker.get_remain_data_offset(); + write_tracker + .update_remaining_after_partial_write(data_segment_len); - debug!( - "Processing successive chunks of a long write packet. \ - Writing : opaque_handle = {:?}, write_offset = {:?}, \ - data_segment = {:?}, data remaining = {:?}", - opaque_handle, - current_write_offset, - data_segment, - write_tracker.get_remain_data_len() - ); + debug!( + "Processing successive chunks of a long write packet. \ + Writing : opaque_handle = {:?}, write_offset = {:?}, \ + data_segment = {:?}, data remaining = {:?}", + opaque_handle, + current_write_offset, + data_segment, + write_tracker.get_remain_data_len() + ); - match self.file_server.write( - &opaque_handle, - current_write_offset, - data_segment.as_ref(), - ) { - Ok(_) => { - if write_tracker.get_remain_data_len() > 0 { - self.partial_write_request_tracker = - Some(write_tracker); - } else { - output_wrapper - .send_status( - write_tracker.get_req_id(), - StatusCode::SSH_FX_OK, - "", - ) - .await?; - info!("Finished multi part Write Request"); - self.state = SftpHandleState::Idle; - } - } - Err(e) => { - error!("SFTP write thrown: {:?}", e); - output_wrapper + match self.file_server.write( + &opaque_handle, + current_write_offset, + data_segment.as_ref(), + ) { + Ok(_) => { + if write_tracker.get_remain_data_len() > 0 { + self.partial_write_request_tracker = + Some(write_tracker); + } else { + output_producer .send_status( write_tracker.get_req_id(), - StatusCode::SSH_FX_FAILURE, - "error writing", + StatusCode::SSH_FX_OK, + "", ) .await?; + info!("Finished multi part Write Request"); self.state = SftpHandleState::Idle; } - }; - buffer_in_lower_index_bracket = - in_len - source.remaining(); - } + } + Err(e) => { + error!("SFTP write thrown: {:?}", e); + output_producer + .send_status( + write_tracker.get_req_id(), + StatusCode::SSH_FX_FAILURE, + "error writing", + ) + .await?; + self.state = SftpHandleState::Idle; + } + }; + buffer_in_lower_index_bracket = in_len - source.remaining(); } - } + }, SftpHandleState::Initializing => { let (source, sftp_packet) = create_sftp_source_and_packet( @@ -383,7 +373,7 @@ where version: SFTP_VERSION, }); - output_wrapper.send_packet(version).await?; + output_producer.send_packet(&version).await?; self.state = SftpHandleState::Idle; } _ => { @@ -414,7 +404,7 @@ where Ok(request) => { Self::handle_general_request( &mut self.file_server, - output_wrapper, + output_producer, request, ) .await?; @@ -428,7 +418,7 @@ where match Self::handle_ran_out( &mut self.file_server, - output_wrapper, + output_producer, &mut source, ) .await @@ -437,7 +427,7 @@ where self.partial_write_request_tracker = Some(holder); self.state = - SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) + SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) } Err(e) => { error!("Error handle_ran_out"); @@ -445,11 +435,12 @@ where SftpError::WireError( WireError::RanOut, ) => { + debug!(""); let read = self.incomplete_request_holder - .try_hold( - &buffer_in - [buffer_in_lower_index_bracket..], - )?; + .try_hold( + &buffer_in + [buffer_in_lower_index_bracket..], + )?; buffer_in_lower_index_bracket += read; self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); @@ -464,7 +455,7 @@ where } _ => { error!("Error decoding SFTP Packet: {:?}", e); - output_wrapper + output_producer .send_status( ReqId(u32::MAX), StatusCode::SSH_FX_OP_UNSUPPORTED, @@ -484,31 +475,49 @@ where /// WIP: A loop that will process all the request from stdio until /// an EOF is received - pub async fn process_loop<'c>( + pub async fn process_loop( &mut self, - stdio: ChanInOut<'c>, + stdio: ChanInOut<'a>, buffer_in: &mut [u8], - buffer_out: &'a mut [u8], + // buffer_out: &'a mut [u8], ) -> SftpResult<()> { let (mut chan_in, chan_out) = stdio.split(); - let mut chan_out_wrapper = - SftpOutputChannelWrapper::new(buffer_out, chan_out); - loop { - let lr = chan_in.read(buffer_in).await?; - trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); - if lr == 0 { - debug!("client disconnected"); - return Err(SftpError::ClientDisconnected); - } + let mut sftp_output_pipe = SftpOutputPipe::::new(); + + let (mut output_consumer, output_producer) = + sftp_output_pipe.split(chan_out); + + let output_consumer_loop = output_consumer.receive_task(); + + let processing_loop = async { + loop { + let lr = chan_in.read(buffer_in).await?; + trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); + if lr == 0 { + debug!("client disconnected"); + return Err(SftpError::ClientDisconnected); + } - self.process(&buffer_in[0..lr], &mut chan_out_wrapper).await?; + self.process(&buffer_in[0..lr], &output_producer).await?; + } + SftpResult::Ok(()) + }; + match select(processing_loop, output_consumer_loop).await { + embassy_futures::select::Either::First(r) => { + debug!("Processing returned: {:?}", r); + r + } + embassy_futures::select::Either::Second(r) => { + warn!("Output consumer returned: {:?}", r); + r + } } } - async fn handle_general_request<'g>( + async fn handle_general_request( file_server: &mut S, - output_wrapper: &mut SftpOutputChannelWrapper<'a, 'g>, + output_producer: &SftpOutputProducer<'_, BUFFER_OUT_SIZE>, request: SftpPacket<'_>, ) -> Result<(), SftpError> where @@ -529,7 +538,7 @@ where &req_id, &response ); - output_wrapper.send_packet(response).await?; + output_producer.send_packet(&response).await?; } SftpPacket::Open(req_id, open) => { match file_server.open(open.filename.as_str()?, &open.attrs) { @@ -540,11 +549,11 @@ where handle: opaque_file_handle.into_file_handle(), }, ); - output_wrapper.send_packet(response).await?; + output_producer.send_packet(&response).await?; } Err(status_code) => { error!("Open failed: {:?}", status_code); - output_wrapper + output_producer .send_status(req_id, StatusCode::SSH_FX_FAILURE, "") .await?; } @@ -559,13 +568,13 @@ where write.data.as_ref(), ) { Ok(_) => { - output_wrapper + output_producer .send_status(req_id, StatusCode::SSH_FX_OK, "") .await?; } Err(e) => { error!("SFTP write thrown: {:?}", e); - output_wrapper + output_producer .send_status( req_id, StatusCode::SSH_FX_FAILURE, @@ -578,13 +587,13 @@ where SftpPacket::Close(req_id, close) => { match file_server.close(&T::try_from(&close.handle)?) { Ok(_) => { - output_wrapper + output_producer .send_status(req_id, StatusCode::SSH_FX_OK, "") .await?; } Err(e) => { error!("SFTP Close thrown: {:?}", e); - output_wrapper + output_producer .send_status( req_id, StatusCode::SSH_FX_FAILURE, @@ -603,11 +612,11 @@ where handle: opaque_file_handle.into_file_handle(), }, ); - output_wrapper.send_packet(response).await?; + output_producer.send_packet(&response).await?; } Err(status_code) => { error!("Open failed: {:?}", status_code); - output_wrapper + output_producer .send_status(req_id, StatusCode::SSH_FX_FAILURE, "") .await?; } @@ -617,26 +626,26 @@ where // TODO Implement the mechanism you are going to use to // handle the list of elements - let mut dir_reply = DirReply::new(req_id, output_wrapper); - - match file_server - .readdir(&T::try_from(&read_dir.handle)?, &mut dir_reply) - { - Ok(_) => { - todo!("Dance starts here"); - } - Err(status_code) => { - error!("Open failed: {:?}", status_code); - // output_wrapper - // .push_status( - // req_id, - // StatusCode::SSH_FX_OP_UNSUPPORTED, - // "Error Reading Directory", - // ) - // .await?; - } - }; - error!("Unsupported Read Dir : {:?}", read_dir); + // let mut dir_reply = DirReply::new(req_id, output_wrapper); + + // match file_server + // .readdir(&T::try_from(&read_dir.handle)?, &mut dir_reply) + // { + // Ok(_) => { + // todo!("Dance starts here"); + // } + // Err(status_code) => { + // error!("Open failed: {:?}", status_code); + // // output_wrapper + // // .push_status( + // // req_id, + // // StatusCode::SSH_FX_OP_UNSUPPORTED, + // // "Error Reading Directory", + // // ) + // // .await?; + // } + // }; + // error!("Unsupported Read Dir : {:?}", read_dir); // return Err(SftpError::NotSupported); // push_unsupported(ReqId(0), sink)?; } @@ -662,11 +671,12 @@ where /// /// **WARNING:** Only `SSH_FXP_WRITE` has been implemented! /// - async fn handle_ran_out<'g>( + async fn handle_ran_out( file_server: &mut S, - output_wrapper: &mut SftpOutputChannelWrapper<'a, 'g>, + output_producer: &SftpOutputProducer<'_, BUFFER_OUT_SIZE>, source: &mut SftpSource<'_>, ) -> SftpResult> { + debug!("Handing Ran out"); let packet_type = source.peak_packet_type()?; match packet_type { SftpNum::SSH_FXP_WRITE => { @@ -707,7 +717,7 @@ where } Err(e) => { error!("SFTP write thrown: {:?}", e); - output_wrapper + output_producer .send_status( req_id, StatusCode::SSH_FX_FAILURE, @@ -735,6 +745,11 @@ fn create_sftp_source_and_packet( buffer_in: &[u8], buffer_in_lower_index_bracket: usize, ) -> (SftpSource<'_>, Result, WireError>) { + debug!( + "Creating a source: lower_index_bracket = {:?}, buffer_len = {:?}", + buffer_in_lower_index_bracket, + buffer_in.len() + ); let mut source = SftpSource::new(&buffer_in[buffer_in_lower_index_bracket..]); let sftp_packet = SftpPacket::decode_request(&mut source); diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 4d456b84..69421ba9 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -8,7 +8,7 @@ use embassy_sync::pipe::{Pipe, Reader as PipeReader, Writer as PipeWriter}; use embedded_io_async::Write; use sunset_async::SunsetRawMutex; -use log::{debug, trace}; +use log::{debug, error, trace}; //// This is the beginning of a new idea: /// I want to pass ref of an item where different methods in the sftphandler can @@ -46,7 +46,6 @@ use log::{debug, trace}; pub struct SftpOutputPipe { pipe: Pipe, - capacity: usize, } /// M: SunsetSunsetRawMutex @@ -59,22 +58,15 @@ impl SftpOutputPipe { /// let output_pipe = SftpOutputPipe::::new(); /// pub fn new() -> Self { - SftpOutputPipe { pipe: Pipe::new(), capacity: N } - } - - /// Returns the inner pipe capacity. This method can be called after - /// split. - pub fn get_capacity(&self) -> usize { - self.capacity + SftpOutputPipe { pipe: Pipe::new() } } // TODO: Check if it panics when called twice - // TODO: Fix Doc links /// Get a Consumer and Producer pair so the producer can send data to the /// output channel without mutable borrows. /// - /// The ['SftpOutputConsumer'] needs to be running to write data to the - /// ['ChanOut'] + /// The [`SftpOutputConsumer`] needs to be running to write data to the + /// [`ChanOut`] /// /// ## Lifetimes /// The lifetime indicates that the lifetime of self, ChanOut and the @@ -88,10 +80,14 @@ impl SftpOutputPipe { (SftpOutputConsumer { reader, ssh_chan_out }, SftpOutputProducer { writer }) } } + +/// Consumer that takes ownership of [`ChanOut`]. It pipes the data received +/// from a [`PipeReader`] into the channel pub struct SftpOutputConsumer<'a, const N: usize> { reader: PipeReader<'a, SunsetRawMutex, N>, ssh_chan_out: ChanOut<'a>, } + impl<'a, const N: usize> SftpOutputConsumer<'a, N> { /// Run it to start the piping pub async fn receive_task(&mut self) -> SftpResult<()> { @@ -99,26 +95,35 @@ impl<'a, const N: usize> SftpOutputConsumer<'a, N> { let mut buf = [0u8; N]; loop { let rl = self.reader.read(&mut buf).await; - debug!("Output Consumer Reader task: Reads {} bytes", rl); - debug!("Output Consumer Reader task: Bytes {:?}", &buf[..rl]); + debug!("Output Consumer: Reads {} bytes", rl); + debug!("Output Consumer: Bytes {:?}", &buf[..rl]); if rl > 0 { self.ssh_chan_out.write_all(&buf[..rl]).await?; + } else { + error!("Output Consumer: Empty array received"); } } } } +/// Producer used to send data to a [`ChanOut`] without the restrictions +/// of mutable borrows #[derive(Clone)] pub struct SftpOutputProducer<'a, const N: usize> { writer: PipeWriter<'a, SunsetRawMutex, N>, } impl<'a, const N: usize> SftpOutputProducer<'a, N> { + /// Send the data encoded in the provided [`SftpSink`] without including + /// the size. + /// + /// Use this when you are sending chunks of data after a valid header pub async fn send_payload(&self, sftp_sink: &SftpSink<'_>) -> SftpResult<()> { let buf = sftp_sink.payload_slice(); Self::send_buffer(&self.writer, &buf).await; Ok(()) } + /// Simplifies the task of sending a status response to the client. pub async fn send_status( &self, req_id: ReqId, @@ -134,7 +139,7 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { Ok(()) } - /// Push an SFTP Packet into the channel out + /// Sends an SFTP Packet into the channel out, including the length field pub async fn send_packet(&self, packet: &SftpPacket<'_>) -> SftpResult<()> { let mut buf = [0u8; N]; let mut sink = SftpSink::new(&mut buf); @@ -145,6 +150,7 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { Ok(()) } + /// Internal associated method to log the writes to the pipe async fn send_buffer(writer: &PipeWriter<'a, SunsetRawMutex, N>, buf: &[u8]) { debug!("Output Producer: Sends {:?} bytes", buf.len()); trace!("Output Producer: Sending buffer {:?}", buf); diff --git a/sftp/src/sftphandler/sftpoutputchannelwrapper.rs b/sftp/src/sftphandler/sftpoutputchannelwrapper.rs deleted file mode 100644 index e9f7033f..00000000 --- a/sftp/src/sftphandler/sftpoutputchannelwrapper.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::error::SftpResult; -use crate::proto::ReqId; -use crate::proto::SftpPacket; -use crate::proto::Status; -use crate::protocol::StatusCode; -use crate::server::SftpSink; - -use sunset::sshwire::SSHEncode; -use sunset::sshwire::WireError; -use sunset_async::ChanOut; - -use embedded_io_async::Write; -#[allow(unused_imports)] -use log::{debug, info, trace, warn}; - -pub struct BufferMsg { - pub data: [u8; N], - pub used: usize, -} -/// Wrapper structure to handle SFTP output operations -/// -/// It wraps an SftpSink and a ChanOut to facilitate sending SFTP packets -/// even when they require multiple iterations -pub struct SftpOutputChannelWrapper<'g> { - sink: SftpSink<'g>, - channel_out: ChanOut<'g>, -} - -impl<'g> SftpOutputChannelWrapper<'g> { - /// Creates a new OutputWrapper - /// - /// This structure wraps an SftpSink and a ChanOut to facilitate - /// sending SFTP packets even when they require multiple steps - pub fn new(buffer: &'g mut [u8], channel_out: ChanOut<'g>) -> Self { - let sink = SftpSink::new(buffer); - SftpOutputChannelWrapper { channel_out, sink } - } - - /// Finalizes (Prepends the packet length) and send the data in the - /// buffer by the subsystem channel out - pub async fn send_buffer(&mut self) -> SftpResult { - if self.sink.payload_len() == 0 { - debug!("No data to send in the SFTP sink"); - return Ok(0); - } - self.sink.finalize(); - let buffer = self.sink.used_slice(); - info!("Sending buffer: '{:?}'", buffer); - let written = self.channel_out.write(buffer).await?; - self.sink.reset(); - Ok(written) - } - - /// Send the data in the buffer by the subsystem channel out without - /// prepending the packet length to it. - /// - /// This is useful when an SFTP packet header has already being sent - /// or when the data requires an special treatment - pub async fn send_payload(&mut self) -> SftpResult { - let payload = self.sink.payload_slice(); - info!("Sending payload: '{:?}'", payload); - let written = self.channel_out.write(payload).await?; - self.sink.reset(); - Ok(written) - } - - /// Push a status message into the channel out - pub async fn send_status( - &mut self, - req_id: ReqId, - status: StatusCode, - msg: &'static str, - ) -> Result<(), WireError> { - let response = SftpPacket::Status( - req_id, - Status { code: status, message: msg.into(), lang: "en-US".into() }, - ); - trace!("Pushing a status message: {:?}", response); - self.send_packet(response); - - Ok(()) - } - - /// Push an SFTP Packet into the channel out - pub async fn send_packet( - &mut self, - packet: SftpPacket<'_>, - ) -> Result<(), WireError> { - packet.encode_response(&mut self.sink)?; - self.send_buffer().await?; - Ok(()) - } - - pub async fn push(&mut self, item: &impl SSHEncode) -> Result<(), WireError> { - item.enc(&mut self.sink)?; - self.send_buffer().await?; - Ok(()) - } -} diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 9e323ad6..a783100a 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,5 +1,4 @@ use crate::error::SftpResult; -use crate::sftphandler::SftpOutputChannelWrapper; use crate::{ handles::OpaqueFileHandle, proto::{Attrs, Name, ReqId, StatusCode}, @@ -179,24 +178,19 @@ pub struct ChanOut<'g, 'a> { pub struct DirReply<'g> { /// Used during the req_id: ReqId, - /// To test muting operations - chan_out: &'g mut SftpOutputChannelWrapper<'g>, + + _phantom_g: PhantomData<&'g ()>, + // /// To test muting operations + // chan_out: &'g mut SftpOutputChannelWrapper<'g>, } impl<'g> DirReply<'g> { - /// I am faking a DirReply to prototype it - // pub fn mock(req_id: ReqId, muting: &'a mut u32) -> Self { - // DirReply { - // chan_out: ChanOut { _phantom_g: PhantomData, _phantom_a: PhantomData }, - // // muting, - // req_id, - // } - // } pub fn new( req_id: ReqId, - chan_out_wrapper: &'g mut SftpOutputChannelWrapper<'g>, + // chan_out_wrapper: &'g mut SftpOutputChannelWrapper<'g>, ) -> Self { - DirReply { chan_out: chan_out_wrapper, req_id } + // DirReply { chan_out: chan_out_wrapper, req_id } + DirReply { req_id, _phantom_g: PhantomData } } // TODO this will need to do async execution @@ -217,10 +211,10 @@ impl<'g> DirReply<'g> { "I will send the header here for request id {:?}: count = {:?}, length = {:?}", self.req_id, get_count, get_encoded_len ); - self.chan_out.push(&get_encoded_len).await?; - self.chan_out.push(&(104 as u8)).await?; - self.chan_out.push(&get_count).await?; - self.chan_out.send_payload().await?; + // self.chan_out.push(&get_encoded_len).await?; + // self.chan_out.push(&(104 as u8)).await?; + // self.chan_out.push(&get_count).await?; + // self.chan_out.send_payload().await?; Ok(()) } } From a84c7d18d776e22abdeea36532ab5c81140ad7a7 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 6 Nov 2025 17:02:31 +1100 Subject: [PATCH 330/393] [skip ci] WIP: write request fails but will be parked for now I am going to focus on listing folder files --- demo/sftp/std/src/main.rs | 13 ++++++++++--- sftp/src/sftphandler/sftpoutputchannelhandler.rs | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index ac251e37..74f1b34b 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -205,14 +205,21 @@ async fn listen( #[embassy_executor::main] async fn main(spawner: Spawner) { env_logger::builder() - .filter_level(log::LevelFilter::Trace) + .filter_level(log::LevelFilter::Info) .filter_module("sunset::runner", log::LevelFilter::Info) + // .filter_module("sunset::runner", log::LevelFilter::Trace) + // .filter_module("sunset::channel", log::LevelFilter::Trace) + .filter_module( + "sunset_sftp::sftphandler::sftpoutputchannelhandler", + log::LevelFilter::Trace, + ) .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) - .filter_module("sunset::traffic", log::LevelFilter::Info) + // .filter_module("sunset::traffic", log::LevelFilter::Trace) .filter_module("sunset::encrypt", log::LevelFilter::Info) .filter_module("sunset::conn", log::LevelFilter::Info) - .filter_module("sunset::kex", log::LevelFilter::Info) + // .filter_module("sunset::kex", log::LevelFilter::Info) + .filter_module("sunset::kex", log::LevelFilter::Trace) .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) .filter_module("async_io", log::LevelFilter::Info) .filter_module("polling", log::LevelFilter::Info) diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 69421ba9..54c07994 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -96,9 +96,9 @@ impl<'a, const N: usize> SftpOutputConsumer<'a, N> { loop { let rl = self.reader.read(&mut buf).await; debug!("Output Consumer: Reads {} bytes", rl); - debug!("Output Consumer: Bytes {:?}", &buf[..rl]); if rl > 0 { self.ssh_chan_out.write_all(&buf[..rl]).await?; + debug!("Output Consumer: Bytes written {:?}", &buf[..rl]); } else { error!("Output Consumer: Empty array received"); } From ae5fdc879211816d0582b09ab43b479124ace592 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 6 Nov 2025 18:22:41 +1100 Subject: [PATCH 331/393] [skip ci] WIP: Some issues in DemoSftpServer DirEntriesCollection but ready to make the SftpServer trait functions async The length calculation seems erroneous but I am more worried about the async trait [skip ci] WIP: Progress with readdir. Needs tyding up Issue with pipe Looks like the number of sent items in the pipe does not match received items. I am going to add a send/receive total bytes counter. It might also be the cause of the write errors [skip ci] WIP: Cargo lock update long forgotten. Apologies --- Cargo.lock | 24 +++--- demo/sftp/std/src/demosftpserver.rs | 52 ++++++------- demo/sftp/std/src/main.rs | 9 ++- sftp/src/sftphandler/mod.rs | 1 + sftp/src/sftphandler/sftphandler.rs | 41 +++++----- .../sftphandler/sftpoutputchannelhandler.rs | 13 +++- sftp/src/sftpserver.rs | 74 +++++++++++++------ 7 files changed, 128 insertions(+), 86 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de7fac7f..500e36e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -805,9 +805,9 @@ dependencies = [ [[package]] name = "embassy-futures" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f878075b9794c1e4ac788c95b728f26aa6366d32eeb10c7051389f898f7d067" +checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01" [[package]] name = "embassy-hal-internal" @@ -934,15 +934,15 @@ dependencies = [ [[package]] name = "embassy-sync" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef1a8a1ea892f9b656de0295532ac5d8067e9830d49ec75076291fd6066b136" +checksum = "73974a3edbd0bd286759b3d483540f0ebef705919a5f56f4fc7709066f71689b" dependencies = [ "cfg-if", "critical-section", "embedded-io-async", + "futures-core", "futures-sink", - "futures-util", "heapless", ] @@ -2777,7 +2777,7 @@ name = "sunset-async" version = "0.3.0" dependencies = [ "embassy-futures", - "embassy-sync 0.7.0", + "embassy-sync 0.7.2", "embedded-io-async", "log", "portable-atomic", @@ -2794,7 +2794,7 @@ dependencies = [ "embassy-futures", "embassy-net", "embassy-net-driver", - "embassy-sync 0.7.0", + "embassy-sync 0.7.2", "embassy-time", "embedded-io-async", "heapless", @@ -2824,7 +2824,7 @@ dependencies = [ "embassy-net", "embassy-net-wiznet", "embassy-rp", - "embassy-sync 0.7.0", + "embassy-sync 0.7.2", "embassy-time", "embassy-usb", "embassy-usb-driver", @@ -2859,7 +2859,7 @@ dependencies = [ "embassy-futures", "embassy-net", "embassy-net-tuntap", - "embassy-sync 0.7.0", + "embassy-sync 0.7.2", "embassy-time", "embedded-io-async", "env_logger", @@ -2885,7 +2885,7 @@ dependencies = [ "embassy-futures", "embassy-net", "embassy-net-tuntap", - "embassy-sync 0.7.0", + "embassy-sync 0.7.2", "embassy-time", "embedded-io-async", "env_logger", @@ -2917,6 +2917,8 @@ dependencies = [ name = "sunset-sftp" version = "0.1.1" dependencies = [ + "embassy-futures", + "embassy-sync 0.7.2", "embedded-io-async", "log", "num_enum 0.7.4", @@ -2941,7 +2943,7 @@ dependencies = [ "argh", "critical-section", "embassy-futures", - "embassy-sync 0.7.0", + "embassy-sync 0.7.2", "embedded-io-adapters", "embedded-io-async", "futures", diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 8264ce4f..b53c5849 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -274,17 +274,17 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } } - fn readdir( + async fn readdir( &mut self, opaque_dir_handle: &DemoOpaqueFileHandle, - visitor: &mut DirReply<'_>, + reply: &DirReply<'_, N>, ) -> SftpOpResult<()> { debug!("read dir for {:?}", opaque_dir_handle); if let PrivatePathHandle::Directory(dir) = self .handles_manager .get_private_as_ref(opaque_dir_handle) - .ok_or(StatusCode::SSH_FX_FAILURE)? + .ok_or(StatusCode::SSH_FX_NO_SUCH_FILE)? { let path_str = dir.path.clone(); debug!("opaque handle found in handles manager: {:?}", path_str); @@ -301,13 +301,9 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { let name_entry_collection = DirEntriesCollection::new(dir_iterator); - visitor.send_header( - name_entry_collection.get_count()?, - name_entry_collection.get_encoded_len()?, - ); + name_entry_collection.send_header(reply).await?; - name_entry_collection - .for_each_encoded(|data: &[u8]| visitor.send_item(data))?; + name_entry_collection.send_entries(reply).await?; } else { error!("the path is not a directory = {:?}", dir_path); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); @@ -316,13 +312,19 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { error!("Could not find the directory for {:?}", opaque_dir_handle); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); } - - error!("What is the return that we are looking for?"); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + Ok(()) } } // TODO Add this to SFTP library only available with std as a global helper +/// This is a helper structure to make ReadDir into something somehow +/// digestible by [`DirReply`] +/// +/// WIP: Not stable. It has know issues and most likely it's methods will change +/// +/// BUG: It does not count properly the number of bytes +/// +/// BUG: It does not include longname and that may be an issue #[derive(Debug)] pub struct DirEntriesCollection { /// Number of elements @@ -396,21 +398,21 @@ impl DirEntriesCollection { ext_count: None, } } -} -impl DirEntriesResponseHelpers for DirEntriesCollection { - fn get_count(&self) -> SftpOpResult { - Ok(self.count) - } - - fn get_encoded_len(&self) -> SftpOpResult { - Ok(self.encoded_length) + pub async fn send_header( + &self, + reply: &DirReply<'_, N>, + ) -> SftpOpResult<()> { + reply.send_header(self.count, self.encoded_length).await.map_err(|e| { + debug!("Could not send header {e:?}"); + StatusCode::SSH_FX_FAILURE + }) } - fn for_each_encoded(&self, mut writer: F) -> SftpOpResult<()> - where - F: FnMut(&[u8]) -> (), - { + pub async fn send_entries( + &self, + reply: &DirReply<'_, N>, + ) -> SftpOpResult<()> { for entry in &self.entries { let filename = entry.path().to_string_lossy().into_owned(); let attrs = Self::get_attrs_or_empty(entry.metadata()); @@ -426,7 +428,7 @@ impl DirEntriesResponseHelpers for DirEntriesCollection { debug!("WireError: {:?}", err); StatusCode::SSH_FX_FAILURE })?; - writer(sftp_sink.payload_slice()); + reply.send_item(sftp_sink.payload_slice()).await; } Ok(()) } diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 74f1b34b..22baf1ec 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -211,14 +211,17 @@ async fn main(spawner: Spawner) { // .filter_module("sunset::channel", log::LevelFilter::Trace) .filter_module( "sunset_sftp::sftphandler::sftpoutputchannelhandler", - log::LevelFilter::Trace, + log::LevelFilter::Debug, ) .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) - // .filter_module("sunset::traffic", log::LevelFilter::Trace) + .filter_module( + "sunset_demo_sftp_std::demosftpserver", + log::LevelFilter::Debug, + ) + .filter_module("sunset_sftp::sftpserver", log::LevelFilter::Debug) .filter_module("sunset::encrypt", log::LevelFilter::Info) .filter_module("sunset::conn", log::LevelFilter::Info) - // .filter_module("sunset::kex", log::LevelFilter::Info) .filter_module("sunset::kex", log::LevelFilter::Trace) .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) .filter_module("async_io", log::LevelFilter::Info) diff --git a/sftp/src/sftphandler/mod.rs b/sftp/src/sftphandler/mod.rs index c7660cd1..6122c204 100644 --- a/sftp/src/sftphandler/mod.rs +++ b/sftp/src/sftphandler/mod.rs @@ -4,3 +4,4 @@ mod sftpoutputchannelhandler; pub use partialwriterequesttracker::PartialWriteRequestTracker; pub use sftphandler::SftpHandler; +pub use sftpoutputchannelhandler::SftpOutputProducer; diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 37bd0a23..bbd34c13 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -7,6 +7,7 @@ use crate::proto::{ SftpPacket, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; +use crate::server::{DirReply, SftpOpResult}; use crate::sftperror::SftpResult; use crate::sftphandler::sftpoutputchannelhandler::{ SftpOutputPipe, SftpOutputProducer, @@ -626,33 +627,27 @@ where // TODO Implement the mechanism you are going to use to // handle the list of elements - // let mut dir_reply = DirReply::new(req_id, output_wrapper); - - // match file_server - // .readdir(&T::try_from(&read_dir.handle)?, &mut dir_reply) - // { - // Ok(_) => { - // todo!("Dance starts here"); - // } - // Err(status_code) => { - // error!("Open failed: {:?}", status_code); - // // output_wrapper - // // .push_status( - // // req_id, - // // StatusCode::SSH_FX_OP_UNSUPPORTED, - // // "Error Reading Directory", - // // ) - // // .await?; - // } - // }; - // error!("Unsupported Read Dir : {:?}", read_dir); - // return Err(SftpError::NotSupported); - // push_unsupported(ReqId(0), sink)?; + let mut dir_reply = DirReply::new(req_id, output_producer); + + match file_server + .readdir(&T::try_from(&read_dir.handle)?, &mut dir_reply) + .await + { + Ok(_) => {} + Err(status) => { + error!("Open failed: {:?}", status); + + output_producer.send_status( + req_id, + status, + "Error Reading Directory", + ); + } + }; } _ => { error!("Unsupported request type: {:?}", request); return Err(SftpError::NotSupported); - // push_unsupported(ReqId(0), sink)?; } } Ok(()) diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 54c07994..78d160cc 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -113,7 +113,16 @@ pub struct SftpOutputProducer<'a, const N: usize> { writer: PipeWriter<'a, SunsetRawMutex, N>, } impl<'a, const N: usize> SftpOutputProducer<'a, N> { - /// Send the data encoded in the provided [`SftpSink`] without including + /// Sends the data encoded in the provided [`SftpSink`] without including + /// the size. + /// + /// Use this when you are sending chunks of data after a valid header + pub async fn send_data(&self, buf: &[u8]) -> SftpResult<()> { + Self::send_buffer(&self.writer, &buf).await; + Ok(()) + } + + /// Sends the data encoded in the provided [`SftpSink`] without including /// the size. /// /// Use this when you are sending chunks of data after a valid header @@ -139,7 +148,7 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { Ok(()) } - /// Sends an SFTP Packet into the channel out, including the length field + /// Sends a SFTP Packet into the channel out, including the length field pub async fn send_packet(&self, packet: &SftpPacket<'_>) -> SftpResult<()> { let mut buf = [0u8; N]; let mut sink = SftpSink::new(&mut buf); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index a783100a..017677fa 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,4 +1,6 @@ use crate::error::SftpResult; +use crate::server::SftpSink; +use crate::sftphandler::SftpOutputProducer; use crate::{ handles::OpaqueFileHandle, proto::{Attrs, Name, ReqId, StatusCode}, @@ -6,6 +8,7 @@ use crate::{ use core::marker::PhantomData; use log::debug; +use sunset::sshwire::SSHEncode; // use futures::executor::block_on; TODO Deal with the async nature of [`ChanOut`] @@ -74,10 +77,10 @@ where /// Reads the list of items in a directory #[allow(unused_variables)] - fn readdir( + async fn readdir( &mut self, opaque_dir_handle: &T, - reply: &mut DirReply<'_>, + reply: &DirReply<'_, N>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", @@ -86,6 +89,15 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } + // async fn readdir( + // &mut self, + // opaque_dir_handle: &T, + // reply: &DirReply<'_, N>, + // ) -> SftpOpResult<()> { + // log::error!("SftpServer ReadDir operation not defined"); + // Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + // } + /// Provides the real path of the directory specified fn realpath(&mut self, dir: &str) -> SftpOpResult> { log::error!("SftpServer RealPath operation not defined: dir = {:?}", dir); @@ -127,6 +139,18 @@ pub trait DirEntriesResponseHelpers { { Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } + + // /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter + // /// were a [`NameEntry`] has been encoded. + // /// + // /// + // #[allow(unused_variables)] + // fn encoded_iter( + // &self, + // writer: F, + // ) -> impl Iterator> { + // Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + // } } // TODO Define this @@ -175,35 +199,31 @@ pub struct ChanOut<'g, 'a> { /// c. Send each serialized `NameEntry`, excluding their length using /// the `send_item` method /// -pub struct DirReply<'g> { +pub struct DirReply<'g, const N: usize> { /// Used during the req_id: ReqId, - _phantom_g: PhantomData<&'g ()>, + // _phantom_g: PhantomData<&'g ()>, // /// To test muting operations - // chan_out: &'g mut SftpOutputChannelWrapper<'g>, + chan_out: &'g SftpOutputProducer<'g, N>, } -impl<'g> DirReply<'g> { - pub fn new( - req_id: ReqId, - // chan_out_wrapper: &'g mut SftpOutputChannelWrapper<'g>, - ) -> Self { +impl<'g, const N: usize> DirReply<'g, N> { + pub fn new(req_id: ReqId, chan_out: &'g SftpOutputProducer<'g, N>) -> Self { // DirReply { chan_out: chan_out_wrapper, req_id } - DirReply { req_id, _phantom_g: PhantomData } + DirReply { req_id, chan_out } } - // TODO this will need to do async execution - /// mocks sending an item via a stdio - pub fn send_item(&mut self, data: &[u8]) { + /// Sends an item to the client + pub async fn send_item(&self, data: &[u8]) { // *self.muting += 1; - debug!("Send item: data = {:?}", data); + debug!("Sending item: len = {:?}, content = {:?}", data.len(), data); + self.chan_out.send_data(data).await; } - // TODO this will need to do async execution - /// Must be call it first. Make this enforceable + /// Sends the header to the client. TODO Make this enforceable pub async fn send_header( - &mut self, + &self, get_count: u32, get_encoded_len: u32, ) -> SftpResult<()> { @@ -211,10 +231,20 @@ impl<'g> DirReply<'g> { "I will send the header here for request id {:?}: count = {:?}, length = {:?}", self.req_id, get_count, get_encoded_len ); - // self.chan_out.push(&get_encoded_len).await?; - // self.chan_out.push(&(104 as u8)).await?; - // self.chan_out.push(&get_count).await?; - // self.chan_out.send_payload().await?; + let mut s = [0u8; N]; + let mut sink = SftpSink::new(&mut s); + + get_encoded_len.enc(&mut sink)?; + 104u8.enc(&mut sink)?; + self.req_id.enc(&mut sink)?; + get_count.enc(&mut sink)?; + let payload = sink.payload_slice(); + debug!( + "Sending header: len = {:?}, content = {:?}", + payload.len(), + payload + ); + self.chan_out.send_data(sink.payload_slice()).await?; Ok(()) } } From 8786e12a7f18a29c571580f100dd5572a2c513ac Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 7 Nov 2025 12:02:31 +1100 Subject: [PATCH 332/393] [skip ci] WIP: FIX read operation Looping to write all bytes. The counters confirm that there is no missing bytes as they match. Tested with a 100MB writing operation: ./test_long_write_requests.sh I am confident that this the write issue is solved --- demo/sftp/std/src/demosftpserver.rs | 12 +- sftp/src/lib.rs | 2 +- sftp/src/sftphandler/sftphandler.rs | 8 +- .../sftphandler/sftpoutputchannelhandler.rs | 97 ++++++++------ sftp/src/sftpserver.rs | 124 +++++++++--------- 5 files changed, 125 insertions(+), 118 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index b53c5849..3d412cc7 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -7,10 +7,7 @@ use sunset::sshwire::SSHEncode; use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; use sunset_sftp::protocol::constants::MAX_NAME_ENTRY_SIZE; use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; -use sunset_sftp::server::{ - DirEntriesResponseHelpers, DirReply, ReadReply, SftpOpResult, SftpServer, - SftpSink, -}; +use sunset_sftp::server::{DirReply, ReadReply, SftpOpResult, SftpServer, SftpSink}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; @@ -425,10 +422,13 @@ impl DirEntriesCollection { let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; let mut sftp_sink = SftpSink::new(&mut buffer); name_entry.enc(&mut sftp_sink).map_err(|err| { - debug!("WireError: {:?}", err); + error!("WireError: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; + reply.send_item(sftp_sink.payload_slice()).await.map_err(|err| { + error!("SftpError: {:?}", err); StatusCode::SSH_FX_FAILURE })?; - reply.send_item(sftp_sink.payload_slice()).await; } Ok(()) } diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 0a9b81b0..accfcab2 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -69,7 +69,7 @@ pub use sftphandler::SftpHandler; /// Structures and types used to add the details for the target system pub mod server { - pub use crate::sftpserver::DirEntriesResponseHelpers; + // pub use crate::sftpserver::DirEntriesResponseHelpers; pub use crate::sftpserver::DirReply; pub use crate::sftpserver::ReadReply; pub use crate::sftpserver::SftpOpResult; diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index bbd34c13..d370cddc 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -637,11 +637,9 @@ where Err(status) => { error!("Open failed: {:?}", status); - output_producer.send_status( - req_id, - status, - "Error Reading Directory", - ); + output_producer + .send_status(req_id, status, "Error Reading Directory") + .await?; } }; } diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 78d160cc..085e86f8 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -2,6 +2,7 @@ use crate::error::SftpResult; use crate::proto::{ReqId, SftpPacket, Status, StatusCode}; use crate::server::SftpSink; +use embassy_sync::mutex::Mutex; use sunset_async::ChanOut; use embassy_sync::pipe::{Pipe, Reader as PipeReader, Writer as PipeWriter}; @@ -10,42 +11,12 @@ use sunset_async::SunsetRawMutex; use log::{debug, error, trace}; -//// This is the beginning of a new idea: -/// I want to pass ref of an item where different methods in the sftphandler can -/// send data down the ChanOut. That would mutate the ChanOut (since it mutates on write) -/// and would violate the basic rule of only one mut borrow. -/// -/// To overcome this hurdle, I can use a two part solution related with a channel or a pipe. -/// -/// -/// Some notes: -/// -/// # sftpoutputchannelwrapper -/// Currently is a mutable entity. That causes issues since it needs to be mutated during loops. -/// ## first usage: -/// push SFTPEncode n times (composition) -// send_payload() -/// ## Second usage: -/// send_packet(SftpPacket) -/// ## last usage: -/// send_status('static str for messages) -> calls send_packet -/// # Alternative to avoid mutation: a channel or pipe -/// What would we put in the pipe? -/// ## 1st. composition: -/// We would create an SftpSink, add the SFTPEncode items and send it as a buffer. Maybe Len field eq to 0? -/// Maybe receive an SftpSink? -/// ## 2nd. SftpPacket -/// SftpSink, encode a packet, terminate it and send the SftpSink. len != 0 -/// ## 3rd. SftpPacket::Status -/// Compose the Status SftpPacket, Encode it in SftpSink, finalise it, send the buffer -// - -// enum AgentMsg { message to be sent} - -// static' RAW_PIPE = Pipe::::new(); +type CounterMutex = Mutex; pub struct SftpOutputPipe { pipe: Pipe, + counter_send: CounterMutex, + counter_recv: CounterMutex, } /// M: SunsetSunsetRawMutex @@ -58,7 +29,11 @@ impl SftpOutputPipe { /// let output_pipe = SftpOutputPipe::::new(); /// pub fn new() -> Self { - SftpOutputPipe { pipe: Pipe::new() } + SftpOutputPipe { + pipe: Pipe::new(), + counter_send: Mutex::::new(0), + counter_recv: Mutex::::new(0), + } } // TODO: Check if it panics when called twice @@ -77,7 +52,10 @@ impl SftpOutputPipe { ssh_chan_out: ChanOut<'a>, ) -> (SftpOutputConsumer<'a, N>, SftpOutputProducer<'a, N>) { let (reader, writer) = self.pipe.split(); - (SftpOutputConsumer { reader, ssh_chan_out }, SftpOutputProducer { writer }) + ( + SftpOutputConsumer { reader, ssh_chan_out, counter: &self.counter_recv }, + SftpOutputProducer { writer, counter: &self.counter_send }, + ) } } @@ -86,6 +64,7 @@ impl SftpOutputPipe { pub struct SftpOutputConsumer<'a, const N: usize> { reader: PipeReader<'a, SunsetRawMutex, N>, ssh_chan_out: ChanOut<'a>, + counter: &'a CounterMutex, } impl<'a, const N: usize> SftpOutputConsumer<'a, N> { @@ -95,10 +74,18 @@ impl<'a, const N: usize> SftpOutputConsumer<'a, N> { let mut buf = [0u8; N]; loop { let rl = self.reader.read(&mut buf).await; - debug!("Output Consumer: Reads {} bytes", rl); + let mut total = 0; + { + let mut lock = self.counter.lock().await; + *lock += rl; + total = *lock; + } + + debug!("Output Consumer: Reads {rl} bytes. Total {total}"); if rl > 0 { self.ssh_chan_out.write_all(&buf[..rl]).await?; - debug!("Output Consumer: Bytes written {:?}", &buf[..rl]); + debug!("Output Consumer: Written {:?} bytes ", &buf[..rl].len()); + trace!("Output Consumer: Bytes written {:?}", &buf[..rl]); } else { error!("Output Consumer: Empty array received"); } @@ -111,6 +98,7 @@ impl<'a, const N: usize> SftpOutputConsumer<'a, N> { #[derive(Clone)] pub struct SftpOutputProducer<'a, const N: usize> { writer: PipeWriter<'a, SunsetRawMutex, N>, + counter: &'a CounterMutex, } impl<'a, const N: usize> SftpOutputProducer<'a, N> { /// Sends the data encoded in the provided [`SftpSink`] without including @@ -118,7 +106,7 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { /// /// Use this when you are sending chunks of data after a valid header pub async fn send_data(&self, buf: &[u8]) -> SftpResult<()> { - Self::send_buffer(&self.writer, &buf).await; + Self::send_buffer(&self.writer, &buf, &self.counter).await; Ok(()) } @@ -128,7 +116,7 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { /// Use this when you are sending chunks of data after a valid header pub async fn send_payload(&self, sftp_sink: &SftpSink<'_>) -> SftpResult<()> { let buf = sftp_sink.payload_slice(); - Self::send_buffer(&self.writer, &buf).await; + Self::send_buffer(&self.writer, &buf, &self.counter).await; Ok(()) } @@ -155,14 +143,37 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { packet.encode_response(&mut sink)?; debug!("Output Producer: Sending packet {:?}", packet); sink.finalize(); - Self::send_buffer(&self.writer, &sink.used_slice()).await; + Self::send_buffer(&self.writer, &sink.used_slice(), &self.counter).await; Ok(()) } /// Internal associated method to log the writes to the pipe - async fn send_buffer(writer: &PipeWriter<'a, SunsetRawMutex, N>, buf: &[u8]) { - debug!("Output Producer: Sends {:?} bytes", buf.len()); + async fn send_buffer( + writer: &PipeWriter<'a, SunsetRawMutex, N>, + buf: &[u8], + counter: &CounterMutex, + ) { + let mut total = 0; + { + let mut lock = counter.lock().await; + *lock += buf.len(); + total = *lock; + } + + debug!("Output Producer: Sends {:?} bytes. Total {total}", buf.len()); trace!("Output Producer: Sending buffer {:?}", buf); - writer.write(buf).await; + + // writer.write_all(buf); // ??? error[E0596]: cannot borrow `*writer` as mutable, as it is behind a `&` reference + + let mut buf = buf; + loop { + if buf.len() == 0 { + break; + } + trace!("Sending buffer {:?}", buf); + + let bytes_sent = writer.write(&buf).await; + buf = &buf[bytes_sent..]; + } } } diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 017677fa..21e2ea64 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -7,7 +7,7 @@ use crate::{ }; use core::marker::PhantomData; -use log::debug; +use log::{debug, trace}; use sunset::sshwire::SSHEncode; // use futures::executor::block_on; TODO Deal with the async nature of [`ChanOut`] @@ -105,53 +105,53 @@ where } } -/// This trait is an standardized way to interact with an iterator or collection of Directory entries -/// that need to be sent via an SSH_FXP_READDIR SFTP response to a client. -/// -/// It uses is expected when implementing an [`SftpServer`] TODO Future trait WIP -pub trait DirEntriesResponseHelpers { - /// returns the number of directory entries. - /// Used for the `SSH_FXP_READDIR` response field `count` - /// as specified in [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) - fn get_count(&self) -> SftpOpResult { - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) - } - - /// Returns the total encoded length in bytes for all directory entries. - /// Used for the `SSH_FXP_READDIR` general response field `length` - /// as part of the [General Packet Format](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-3) - /// - /// This represents the sum of all [`NameEntry`] structures when encoded - /// into [`SftpSink`] format. The length is to be pre-computed by - /// encoding each entry and summing the [`SftpSink::payload_len()`] values. - fn get_encoded_len(&self) -> SftpOpResult { - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) - } - - /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter - /// were a [`NameEntry`] has been encoded. - /// - /// - #[allow(unused_variables)] - fn for_each_encoded(&self, writer: F) -> SftpOpResult<()> - where - F: FnMut(&[u8]) -> (), - { - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) - } - - // /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter - // /// were a [`NameEntry`] has been encoded. - // /// - // /// - // #[allow(unused_variables)] - // fn encoded_iter( - // &self, - // writer: F, - // ) -> impl Iterator> { - // Err(StatusCode::SSH_FX_OP_UNSUPPORTED) - // } -} +// /// This trait is an standardized way to interact with an iterator or collection of Directory entries +// /// that need to be sent via an SSH_FXP_READDIR SFTP response to a client. +// /// +// /// It uses is expected when implementing an [`SftpServer`] TODO Future trait WIP +// pub trait DirEntriesResponseHelpers { +// /// returns the number of directory entries. +// /// Used for the `SSH_FXP_READDIR` response field `count` +// /// as specified in [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) +// fn get_count(&self) -> SftpOpResult { +// Err(StatusCode::SSH_FX_OP_UNSUPPORTED) +// } + +// /// Returns the total encoded length in bytes for all directory entries. +// /// Used for the `SSH_FXP_READDIR` general response field `length` +// /// as part of the [General Packet Format](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-3) +// /// +// /// This represents the sum of all [`NameEntry`] structures when encoded +// /// into [`SftpSink`] format. The length is to be pre-computed by +// /// encoding each entry and summing the [`SftpSink::payload_len()`] values. +// fn get_encoded_len(&self) -> SftpOpResult { +// Err(StatusCode::SSH_FX_OP_UNSUPPORTED) +// } + +// /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter +// /// were a [`NameEntry`] has been encoded. +// /// +// /// +// #[allow(unused_variables)] +// fn for_each_encoded(&self, writer: F) -> SftpOpResult<()> +// where +// F: FnMut(&[u8]) -> (), +// { +// Err(StatusCode::SSH_FX_OP_UNSUPPORTED) +// } + +// // /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter +// // /// were a [`NameEntry`] has been encoded. +// // /// +// // /// +// // #[allow(unused_variables)] +// // fn encoded_iter( +// // &self, +// // writer: F, +// // ) -> impl Iterator> { +// // Err(StatusCode::SSH_FX_OP_UNSUPPORTED) +// // } +// } // TODO Define this /// **This is a work in progress** @@ -185,40 +185,38 @@ pub struct ChanOut<'g, 'a> { /// implementation via [`SftpServer::readdir()`] in order to send the /// directory content list. /// -/// It contains references to a slice buffer and a output channel +/// It handles immutable sending data via the underlying sftp-channel /// [`sunset_async::async_channel::ChanOut`] used in the context of an /// SFTP Session. /// /// The usage is simple: /// -/// 1. SftpHandler will: Initialize the structure with the buffer and the ChanOut +/// 1. SftpHandler will: Initialize the structure /// 2. The `SftpServer` trait implementation for `readdir()` will: -/// a. Receive the DirReply -/// b. Obtain the number of items and the **total** encoded length of -/// the [`NameEntry`] items in the directory and send them calling `send_header` -/// c. Send each serialized `NameEntry`, excluding their length using -/// the `send_item` method -/// +/// a. Receive the DirReply ref `reply` +/// b. Instantiate a [`DirEntriesCollection`] with the items in the requested folder +/// c. call the `DirEntriesCollection.SendHeader(reply)` +/// d. call the `DirEntriesCollection.send_entries(reply)` pub struct DirReply<'g, const N: usize> { - /// Used during the + /// The request Id that will be used in the response req_id: ReqId, - // _phantom_g: PhantomData<&'g ()>, - // /// To test muting operations + /// Immutable writer chan_out: &'g SftpOutputProducer<'g, N>, } impl<'g, const N: usize> DirReply<'g, N> { + /// New instance pub fn new(req_id: ReqId, chan_out: &'g SftpOutputProducer<'g, N>) -> Self { // DirReply { chan_out: chan_out_wrapper, req_id } DirReply { req_id, chan_out } } /// Sends an item to the client - pub async fn send_item(&self, data: &[u8]) { - // *self.muting += 1; - debug!("Sending item: len = {:?}, content = {:?}", data.len(), data); - self.chan_out.send_data(data).await; + pub async fn send_item(&self, data: &[u8]) -> SftpResult<()> { + debug!("Sending item: {:?} bytes", data.len()); + trace!("Sending item: content = {:?}", data); + self.chan_out.send_data(data).await } /// Sends the header to the client. TODO Make this enforceable From acee5276c6e3f2610565f9dc7ddadc1f4009986b Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 7 Nov 2025 12:12:02 +1100 Subject: [PATCH 333/393] [skip ci] WIP: Hack readdir response was not adding header field length packet type + request id + items (1 + 4 + 4 bytes) hardcoded but will refactor --- demo/sftp/std/src/demosftpserver.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 3d412cc7..dff9f82f 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -334,9 +334,9 @@ pub struct DirEntriesCollection { impl DirEntriesCollection { pub fn new(dir_iterator: fs::ReadDir) -> Self { - let mut encoded_length = 0; - // This way I collect data required for the header and collect - // valid entries into a vector (only std) + let mut encoded_length = 9; // TODO We need to consider the packet type, Id and count fields + // This way I collect data required for the header and collect + // valid entries into a vector (only std) let entries: Vec = dir_iterator .filter_map(|entry_result| { let entry = entry_result.ok()?; From 3ba5410a0af9055222c4e4c321a894a3192f9420 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 7 Nov 2025 12:31:14 +1100 Subject: [PATCH 334/393] [skip ci] WIP: BUG in sftphandler process keeps responding to the same read dir request [skip ci] WIP: BUG **Sending loop includes sftp client** - the ReqId increases - The loop stop in a client interruption [ctl]+C I am going to need more visibility on why the client keeps on sending new request for the same files [skip ci] WIP: BUG I was not following V3 specification as did not send EOF https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7 Next issue is that I am sending the relative path and not just the filename. Will fix next --- demo/sftp/std/src/demosftpserver.rs | 5 + demo/sftp/std/src/main.rs | 37 ++++--- sftp/src/sftphandler/sftphandler.rs | 101 ++++++++---------- .../sftphandler/sftpoutputchannelhandler.rs | 2 +- sftp/src/sftpserver.rs | 22 ++-- 5 files changed, 84 insertions(+), 83 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index dff9f82f..091a0cfe 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -429,6 +429,11 @@ impl DirEntriesCollection { error!("SftpError: {:?}", err); StatusCode::SSH_FX_FAILURE })?; + + reply.send_eof().await.map_err(|err| { + error!("SftpError: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; } Ok(()) } diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 22baf1ec..84348a6c 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -205,28 +205,27 @@ async fn listen( #[embassy_executor::main] async fn main(spawner: Spawner) { env_logger::builder() - .filter_level(log::LevelFilter::Info) - .filter_module("sunset::runner", log::LevelFilter::Info) - // .filter_module("sunset::runner", log::LevelFilter::Trace) - // .filter_module("sunset::channel", log::LevelFilter::Trace) - .filter_module( - "sunset_sftp::sftphandler::sftpoutputchannelhandler", - log::LevelFilter::Debug, - ) - .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) - .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) + .filter_level(log::LevelFilter::Warn) .filter_module( "sunset_demo_sftp_std::demosftpserver", - log::LevelFilter::Debug, + log::LevelFilter::Info, ) - .filter_module("sunset_sftp::sftpserver", log::LevelFilter::Debug) - .filter_module("sunset::encrypt", log::LevelFilter::Info) - .filter_module("sunset::conn", log::LevelFilter::Info) - .filter_module("sunset::kex", log::LevelFilter::Trace) - .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) - .filter_module("async_io", log::LevelFilter::Info) - .filter_module("polling", log::LevelFilter::Info) - .filter_module("embassy_net", log::LevelFilter::Info) + .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Debug) + // .filter_module( + // "sunset_sftp::sftphandler::sftpoutputchannelhandler", + // log::LevelFilter::Trace, + // ) + // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) + // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) + // .filter_module("sunset_sftp::sftpserver", log::LevelFilter::Info) + // .filter_module("sunset::runner", log::LevelFilter::Info) + // .filter_module("sunset::encrypt", log::LevelFilter::Info) + // .filter_module("sunset::conn", log::LevelFilter::Info) + // .filter_module("sunset::kex", log::LevelFilter::Info) + // .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) + // .filter_module("async_io", log::LevelFilter::Info) + // .filter_module("polling", log::LevelFilter::Info) + // .filter_module("embassy_net", log::LevelFilter::Info) .format_timestamp_nanos() .init(); diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index d370cddc..e1d8a26b 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -151,25 +151,22 @@ where buffer_in: &[u8], output_producer: &SftpOutputProducer<'_, BUFFER_OUT_SIZE>, ) -> SftpResult<()> { - let in_len = buffer_in.len(); - let mut buffer_in_lower_index_bracket = 0; + let mut buf = buffer_in; - trace!("Received {:} bytes to process", in_len); + trace!("Received {:} bytes to process", buf.len()); if !matches!(self.state, SftpHandleState::Fragmented(_)) - & in_len.lt(&SFTP_MINIMUM_PACKET_LEN) + & buf.len().lt(&SFTP_MINIMUM_PACKET_LEN) { return Err(WireError::PacketWrong.into()); } - while buffer_in_lower_index_bracket < in_len { + trace!("Entering loop to process the full received buffer"); + while buf.len() > 0 { debug!( - "Buffer In Lower index bracket: {}", - buffer_in_lower_index_bracket - ); - debug!( - "<=======================[ SFTP Process State: {:?} ]=======================>", - self.state + "<=======================[ SFTP Process State: {:?} ]=======================> Buffer remaining: {}", + self.state, + buf.len() ); match &self.state { @@ -178,9 +175,7 @@ where FragmentedRequestState::ProcessingClippedRequest => { if let Err(e) = self .incomplete_request_holder - .try_append_for_valid_request( - &buffer_in[buffer_in_lower_index_bracket..], - ) + .try_append_for_valid_request(&buf) { match e { RequestHolderError::RanOut => { @@ -188,8 +183,9 @@ where "There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); - buffer_in_lower_index_bracket += - self.incomplete_request_holder.appended(); + buf = &buf[self + .incomplete_request_holder + .appended()..]; continue; } RequestHolderError::WireError(WireError::RanOut) => { @@ -197,8 +193,9 @@ where "WIRE ERROR: There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); - buffer_in_lower_index_bracket += - self.incomplete_request_holder.appended(); + buf = &buf[self + .incomplete_request_holder + .appended()..]; continue; } RequestHolderError::NoRoom => { @@ -223,7 +220,7 @@ where } let used = self.incomplete_request_holder.appended(); - buffer_in_lower_index_bracket += used; + buf = &buf[used..]; let mut source = SftpSource::new( &self.incomplete_request_holder.try_get_ref()?, @@ -279,9 +276,7 @@ where } } FragmentedRequestState::ProcessingLongRequest => { - let mut source = SftpSource::new( - &buffer_in[buffer_in_lower_index_bracket..], - ); + let mut source = SftpSource::new(&buf); trace!("Source content: {:?}", source); let mut write_tracker = if let Some(wt) = @@ -356,15 +351,12 @@ where self.state = SftpHandleState::Idle; } }; - buffer_in_lower_index_bracket = in_len - source.remaining(); + buf = &buf[buf.len() - source.remaining()..]; } }, SftpHandleState::Initializing => { - let (source, sftp_packet) = create_sftp_source_and_packet( - buffer_in, - buffer_in_lower_index_bracket, - ); + let (source, sftp_packet) = create_sftp_source_and_packet(buf); match sftp_packet { Ok(request) => { match request { @@ -394,13 +386,11 @@ where return Err(SftpError::MalformedPacket); } } - buffer_in_lower_index_bracket = in_len - source.remaining(); + buf = &buf[buf.len() - source.remaining()..]; } SftpHandleState::Idle => { - let (mut source, sftp_packet) = create_sftp_source_and_packet( - buffer_in, - buffer_in_lower_index_bracket, - ); + let (mut source, sftp_packet) = + create_sftp_source_and_packet(buf); match sftp_packet { Ok(request) => { Self::handle_general_request( @@ -427,8 +417,7 @@ where Ok(holder) => { self.partial_write_request_tracker = Some(holder); - self.state = - SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest) + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); } Err(e) => { error!("Error handle_ran_out"); @@ -436,14 +425,11 @@ where SftpError::WireError( WireError::RanOut, ) => { - debug!(""); - let read = self.incomplete_request_holder - .try_hold( - &buffer_in - [buffer_in_lower_index_bracket..], - )?; - buffer_in_lower_index_bracket += - read; + let read = self + .incomplete_request_holder + .try_hold(&buf)?; + buf = &buf[read..]; + self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); continue; } @@ -466,11 +452,13 @@ where } }, }; - buffer_in_lower_index_bracket = in_len - source.remaining(); + buf = &buf[buf.len() - source.remaining()..]; + trace!("New buffer len {} bytes ", buf.len()) } } + trace!("Process checking buf len {:?}", buf.len()); } - + trace!("Exiting process with Ok(())"); Ok(()) } @@ -494,6 +482,8 @@ where let processing_loop = async { loop { let lr = chan_in.read(buffer_in).await?; + + debug!("SFTP <---- received: {:?} bytes", lr); trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); if lr == 0 { debug!("client disconnected"); @@ -627,13 +617,18 @@ where // TODO Implement the mechanism you are going to use to // handle the list of elements - let mut dir_reply = DirReply::new(req_id, output_producer); + let dir_reply = DirReply::new(req_id, output_producer); match file_server - .readdir(&T::try_from(&read_dir.handle)?, &mut dir_reply) + .readdir(&T::try_from(&read_dir.handle)?, &dir_reply) .await { - Ok(_) => {} + Ok(_) => { + // dir_reply should have sent a response + // output_producer + // .send_status(req_id, StatusCode::SSH_FX_EOF, "") + // .await?; + } Err(status) => { error!("Open failed: {:?}", status); @@ -732,18 +727,14 @@ where // Ok(()) } } + /// Function to create an SFTP source and decode an SFTP packet from it /// to avoid code duplication fn create_sftp_source_and_packet( - buffer_in: &[u8], - buffer_in_lower_index_bracket: usize, + buf: &[u8], ) -> (SftpSource<'_>, Result, WireError>) { - debug!( - "Creating a source: lower_index_bracket = {:?}, buffer_len = {:?}", - buffer_in_lower_index_bracket, - buffer_in.len() - ); - let mut source = SftpSource::new(&buffer_in[buffer_in_lower_index_bracket..]); + debug!("Creating a source: buf_len = {:?}", buf.len()); + let mut source = SftpSource::new(&buf); let sftp_packet = SftpPacket::decode_request(&mut source); (source, sftp_packet) diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 085e86f8..ad2e8a30 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -131,7 +131,7 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { req_id, Status { code: status, message: msg.into(), lang: "en-US".into() }, ); - debug!("Output Producer: Pushing a status message: {:?}", response); + trace!("Output Producer: Pushing a status message: {:?}", response); self.send_packet(&response).await?; Ok(()) } diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 21e2ea64..2482024b 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -197,6 +197,7 @@ pub struct ChanOut<'g, 'a> { /// b. Instantiate a [`DirEntriesCollection`] with the items in the requested folder /// c. call the `DirEntriesCollection.SendHeader(reply)` /// d. call the `DirEntriesCollection.send_entries(reply)` +/// e. call the `reply.send_eof()` pub struct DirReply<'g, const N: usize> { /// The request Id that will be used in the response req_id: ReqId, @@ -212,13 +213,6 @@ impl<'g, const N: usize> DirReply<'g, N> { DirReply { req_id, chan_out } } - /// Sends an item to the client - pub async fn send_item(&self, data: &[u8]) -> SftpResult<()> { - debug!("Sending item: {:?} bytes", data.len()); - trace!("Sending item: content = {:?}", data); - self.chan_out.send_data(data).await - } - /// Sends the header to the client. TODO Make this enforceable pub async fn send_header( &self, @@ -233,7 +227,7 @@ impl<'g, const N: usize> DirReply<'g, N> { let mut sink = SftpSink::new(&mut s); get_encoded_len.enc(&mut sink)?; - 104u8.enc(&mut sink)?; + 104u8.enc(&mut sink)?; // TODO Remove hack self.req_id.enc(&mut sink)?; get_count.enc(&mut sink)?; let payload = sink.payload_slice(); @@ -245,4 +239,16 @@ impl<'g, const N: usize> DirReply<'g, N> { self.chan_out.send_data(sink.payload_slice()).await?; Ok(()) } + + /// Sends an item to the client + pub async fn send_item(&self, data: &[u8]) -> SftpResult<()> { + debug!("Sending item: {:?} bytes", data.len()); + trace!("Sending item: content = {:?}", data); + self.chan_out.send_data(data).await + } + + /// Sends EOF meaning that there is no more files in the directory + pub async fn send_eof(&self) -> SftpResult<()> { + self.chan_out.send_status(self.req_id, StatusCode::SSH_FX_EOF, "").await + } } From 468d117355ea2af686c26ec4a3e6750320a258ef Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 7 Nov 2025 19:05:43 +1100 Subject: [PATCH 335/393] [skip ci] WIP: BUG The EOF needs to be sent once the readdir has sent all the items and is asked again. Anything else fails This is point I have not considered. I could have sent one item at a time, with different challenges. In my opinion I should add this behavior to the SftpServer implementation, but that passes the responsibility to the library user and the idea is making the user not needing to care too much about the protocol implementation [skip ci] WIP: Typo and cleanup commented out code --- demo/sftp/std/src/demosftpserver.rs | 26 ++++++---- sftp/src/sftphandler/sftphandler.rs | 14 ++++-- sftp/src/sftpserver.rs | 74 +++++------------------------ 3 files changed, 38 insertions(+), 76 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 091a0cfe..e0a93183 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -298,9 +298,11 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { let name_entry_collection = DirEntriesCollection::new(dir_iterator); - name_entry_collection.send_header(reply).await?; + name_entry_collection.send_entries_header(reply).await?; name_entry_collection.send_entries(reply).await?; + + // name_entry_collection.send_eof(reply).await?; } else { error!("the path is not a directory = {:?}", dir_path); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); @@ -340,8 +342,7 @@ impl DirEntriesCollection { let entries: Vec = dir_iterator .filter_map(|entry_result| { let entry = entry_result.ok()?; - - let filename = entry.path().to_string_lossy().into_owned(); + let filename = entry.file_name().to_string_lossy().into_owned(); let name_entry = NameEntry { filename: Filename::from(filename.as_str()), _longname: Filename::from(""), @@ -396,7 +397,7 @@ impl DirEntriesCollection { } } - pub async fn send_header( + pub async fn send_entries_header( &self, reply: &DirReply<'_, N>, ) -> SftpOpResult<()> { @@ -411,7 +412,7 @@ impl DirEntriesCollection { reply: &DirReply<'_, N>, ) -> SftpOpResult<()> { for entry in &self.entries { - let filename = entry.path().to_string_lossy().into_owned(); + let filename = entry.file_name().to_string_lossy().into_owned(); let attrs = Self::get_attrs_or_empty(entry.metadata()); let name_entry = NameEntry { filename: Filename::from(filename.as_str()), @@ -429,12 +430,17 @@ impl DirEntriesCollection { error!("SftpError: {:?}", err); StatusCode::SSH_FX_FAILURE })?; - - reply.send_eof().await.map_err(|err| { - error!("SftpError: {:?}", err); - StatusCode::SSH_FX_FAILURE - })?; } Ok(()) } + + pub async fn no_files( + &self, + reply: &DirReply<'_, N>, + ) -> SftpOpResult<()> { + reply.send_eof().await.map_err(|err| { + error!("SftpError: {:?}", err); + StatusCode::SSH_FX_FAILURE + }) + } } diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index e1d8a26b..f55d3285 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -614,9 +614,14 @@ where }; } SftpPacket::ReadDir(req_id, read_dir) => { - // TODO Implement the mechanism you are going to use to - // handle the list of elements - + // TODO I should send back an EOF response when all the files in folder have been sent AND I have been asked for more files. + // According to https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7 + // This should be the file_server responsibility + output_producer + .send_status(req_id, StatusCode::SSH_FX_EOF, "") + .await?; + + return Ok(()); let dir_reply = DirReply::new(req_id, output_producer); match file_server @@ -624,6 +629,7 @@ where .await { Ok(_) => { + // dir_reply should have sent a response // output_producer // .send_status(req_id, StatusCode::SSH_FX_EOF, "") @@ -646,7 +652,7 @@ where Ok(()) } - // TODO Handle more long requests + // TODO Handle other long requests /// Some long request will not fit in the channel buffers. Such requests /// will require to be handled differently. Gathering the data in and /// processing it as we receive it in the channel in buffer. diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 2482024b..227444a8 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -89,15 +89,6 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - // async fn readdir( - // &mut self, - // opaque_dir_handle: &T, - // reply: &DirReply<'_, N>, - // ) -> SftpOpResult<()> { - // log::error!("SftpServer ReadDir operation not defined"); - // Err(StatusCode::SSH_FX_OP_UNSUPPORTED) - // } - /// Provides the real path of the directory specified fn realpath(&mut self, dir: &str) -> SftpOpResult> { log::error!("SftpServer RealPath operation not defined: dir = {:?}", dir); @@ -105,54 +96,6 @@ where } } -// /// This trait is an standardized way to interact with an iterator or collection of Directory entries -// /// that need to be sent via an SSH_FXP_READDIR SFTP response to a client. -// /// -// /// It uses is expected when implementing an [`SftpServer`] TODO Future trait WIP -// pub trait DirEntriesResponseHelpers { -// /// returns the number of directory entries. -// /// Used for the `SSH_FXP_READDIR` response field `count` -// /// as specified in [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) -// fn get_count(&self) -> SftpOpResult { -// Err(StatusCode::SSH_FX_OP_UNSUPPORTED) -// } - -// /// Returns the total encoded length in bytes for all directory entries. -// /// Used for the `SSH_FXP_READDIR` general response field `length` -// /// as part of the [General Packet Format](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-3) -// /// -// /// This represents the sum of all [`NameEntry`] structures when encoded -// /// into [`SftpSink`] format. The length is to be pre-computed by -// /// encoding each entry and summing the [`SftpSink::payload_len()`] values. -// fn get_encoded_len(&self) -> SftpOpResult { -// Err(StatusCode::SSH_FX_OP_UNSUPPORTED) -// } - -// /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter -// /// were a [`NameEntry`] has been encoded. -// /// -// /// -// #[allow(unused_variables)] -// fn for_each_encoded(&self, writer: F) -> SftpOpResult<()> -// where -// F: FnMut(&[u8]) -> (), -// { -// Err(StatusCode::SSH_FX_OP_UNSUPPORTED) -// } - -// // /// Must call the callback passing an [`SftpSink::payload_slice()`] as a parameter -// // /// were a [`NameEntry`] has been encoded. -// // /// -// // /// -// // #[allow(unused_variables)] -// // fn encoded_iter( -// // &self, -// // writer: F, -// // ) -> impl Iterator> { -// // Err(StatusCode::SSH_FX_OP_UNSUPPORTED) -// // } -// } - // TODO Define this /// **This is a work in progress** /// A reference structure passed to the [`SftpServer::read()`] method to @@ -193,11 +136,18 @@ pub struct ChanOut<'g, 'a> { /// /// 1. SftpHandler will: Initialize the structure /// 2. The `SftpServer` trait implementation for `readdir()` will: -/// a. Receive the DirReply ref `reply` -/// b. Instantiate a [`DirEntriesCollection`] with the items in the requested folder -/// c. call the `DirEntriesCollection.SendHeader(reply)` -/// d. call the `DirEntriesCollection.send_entries(reply)` -/// e. call the `reply.send_eof()` +/// +/// - Receive the DirReply ref `reply` +/// +/// a. If there are items to send: +/// +/// - Instantiate a [`DirEntriesCollection`] with the items in the requested folder +/// - call the `DirEntriesCollection.SendHeader(reply)` +/// - call the `DirEntriesCollection.send_entries(reply)` +/// +/// b. If there are no items to send: +/// +/// - Call the `reply.send_eof()` pub struct DirReply<'g, const N: usize> { /// The request Id that will be used in the response req_id: ReqId, From 90c76ced0d23f4c87bb26759af914e6b2d06eb71 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 7 Nov 2025 19:56:48 +1100 Subject: [PATCH 336/393] [skip ci] WIP: Added a simplistic last read status approach Found a bug sending back the status code. It is always Ok! [skip ci] WIP: Temporary ways to track completed Read operations I will add a test to understand the unexpected behaviour in response status packet --- demo/sftp/std/src/demosftpserver.rs | 8 +++-- demo/sftp/std/src/main.rs | 8 ++--- sftp/src/lib.rs | 1 + sftp/src/sftphandler/sftphandler.rs | 52 ++++++++++++++++++++++------- sftp/src/sftpserver.rs | 19 ++++++++++- 5 files changed, 68 insertions(+), 20 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index e0a93183..f7fde1cc 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -7,7 +7,9 @@ use sunset::sshwire::SSHEncode; use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; use sunset_sftp::protocol::constants::MAX_NAME_ENTRY_SIZE; use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; -use sunset_sftp::server::{DirReply, ReadReply, SftpOpResult, SftpServer, SftpSink}; +use sunset_sftp::server::{ + DirReply, ReadReply, ReadStatus, SftpOpResult, SftpServer, SftpSink, +}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; @@ -275,7 +277,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { &mut self, opaque_dir_handle: &DemoOpaqueFileHandle, reply: &DirReply<'_, N>, - ) -> SftpOpResult<()> { + ) -> SftpOpResult { debug!("read dir for {:?}", opaque_dir_handle); if let PrivatePathHandle::Directory(dir) = self @@ -303,6 +305,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { name_entry_collection.send_entries(reply).await?; // name_entry_collection.send_eof(reply).await?; + Ok(ReadStatus::EndOfFile) } else { error!("the path is not a directory = {:?}", dir_path); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); @@ -311,7 +314,6 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { error!("Could not find the directory for {:?}", opaque_dir_handle); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); } - Ok(()) } } diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 84348a6c..ff7cbd4c 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -211,10 +211,10 @@ async fn main(spawner: Spawner) { log::LevelFilter::Info, ) .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Debug) - // .filter_module( - // "sunset_sftp::sftphandler::sftpoutputchannelhandler", - // log::LevelFilter::Trace, - // ) + .filter_module( + "sunset_sftp::sftphandler::sftpoutputchannelhandler", + log::LevelFilter::Trace, + ) // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) // .filter_module("sunset_sftp::sftpserver", log::LevelFilter::Info) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index accfcab2..751b0fa9 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -72,6 +72,7 @@ pub mod server { // pub use crate::sftpserver::DirEntriesResponseHelpers; pub use crate::sftpserver::DirReply; pub use crate::sftpserver::ReadReply; + pub use crate::sftpserver::ReadStatus; pub use crate::sftpserver::SftpOpResult; pub use crate::sftpserver::SftpServer; pub use crate::sftpsink::SftpSink; diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index f55d3285..230355a7 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -4,10 +4,10 @@ use crate::error::SftpError; use crate::handles::OpaqueFileHandle; use crate::proto::{ self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, - SftpPacket, StatusCode, + SftpPacket, Status, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; -use crate::server::{DirReply, SftpOpResult}; +use crate::server::{DirReply, ReadStatus, SftpOpResult, SftpSink}; use crate::sftperror::SftpResult; use crate::sftphandler::sftpoutputchannelhandler::{ SftpOutputPipe, SftpOutputProducer, @@ -17,7 +17,7 @@ use crate::sftpsource::SftpSource; use embassy_futures::select::select; use sunset::Error as SunsetError; -use sunset::sshwire::{SSHSource, WireError}; +use sunset::sshwire::{SSHEncode, SSHSource, WireError}; use sunset_async::ChanInOut; use core::u32; @@ -108,6 +108,12 @@ where partial_write_request_tracker: Option>, incomplete_request_holder: RequestHolder<'a>, + + /// Records the last read status reported + /// from a read operation. This is used to communicate + /// the EOF with the appropriate ReqId to a pending read operation + /// **WARNING**: This assumes that only one read operation is pending at a time. + last_read_status: ReadStatus, } impl<'a, T, S, const BUFFER_OUT_SIZE: usize> SftpHandler<'a, T, S, BUFFER_OUT_SIZE> @@ -132,6 +138,7 @@ where partial_write_request_tracker: None, state: SftpHandleState::default(), incomplete_request_holder: RequestHolder::new(incomplete_request_buffer), + last_read_status: ReadStatus::PendingData, } } @@ -231,6 +238,7 @@ where Ok(request) => { Self::handle_general_request( &mut self.file_server, + &mut self.last_read_status, output_producer, request, ) @@ -395,6 +403,7 @@ where Ok(request) => { Self::handle_general_request( &mut self.file_server, + &mut self.last_read_status, output_producer, request, ) @@ -508,6 +517,7 @@ where async fn handle_general_request( file_server: &mut S, + last_read_status: &mut ReadStatus, output_producer: &SftpOutputProducer<'_, BUFFER_OUT_SIZE>, request: SftpPacket<'_>, ) -> Result<(), SftpError> @@ -617,23 +627,41 @@ where // TODO I should send back an EOF response when all the files in folder have been sent AND I have been asked for more files. // According to https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7 // This should be the file_server responsibility - output_producer - .send_status(req_id, StatusCode::SSH_FX_EOF, "") - .await?; - return Ok(()); + if (*last_read_status).eq(&ReadStatus::EndOfFile) { + let packet = SftpPacket::Status( + req_id, + Status { + code: StatusCode::SSH_FX_EOF, + message: "".into(), + lang: "en-US".into(), + }, + ); + let mut buf = [0u8; 256]; + let mut sink = SftpSink::new(&mut buf); + packet.encode_response(&mut sink)?; + debug!("Output Producer: Sending packet {:?}", packet); + sink.finalize(); + output_producer.send_data(sink.used_slice()).await?; + // output_producer + // .send_status(req_id, StatusCode::SSH_FX_EOF, "") + // .await?; + *last_read_status = ReadStatus::PendingData; + return Ok(()); + } + let dir_reply = DirReply::new(req_id, output_producer); match file_server .readdir(&T::try_from(&read_dir.handle)?, &dir_reply) .await { - Ok(_) => { - + Ok(read_status) => { + *last_read_status = read_status; // dir_reply should have sent a response - // output_producer - // .send_status(req_id, StatusCode::SSH_FX_EOF, "") - // .await?; + output_producer + .send_status(req_id, StatusCode::SSH_FX_EOF, "") + .await?; } Err(status) => { error!("Open failed: {:?}", status); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 227444a8..a2038a6d 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -15,6 +15,22 @@ use sunset::sshwire::SSHEncode; /// Result used to store the result of an Sftp Operation pub type SftpOpResult = core::result::Result; +/// Since the server needs to answer with an STATUS EOF to finish read requests, +/// Helps handling the completion for reading data. +/// See: +/// +/// - [Reading and Writing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) +/// - [Scanning Directories](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7) +#[derive(PartialEq, Debug, Default)] +pub enum ReadStatus { + // TODO Ideally this will contain an OwnedFileHandle + /// There is more data to read + #[default] + PendingData, + /// The server has provided all the data requested + EndOfFile, +} + /// All trait functions are optional in the SFTP protocol. /// Some less core operations have a Provided implementation returning /// returns `SSH_FX_OP_UNSUPPORTED`. Common operations must be implemented, @@ -81,7 +97,7 @@ where &mut self, opaque_dir_handle: &T, reply: &DirReply<'_, N>, - ) -> SftpOpResult<()> { + ) -> SftpOpResult { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", opaque_dir_handle @@ -132,6 +148,7 @@ pub struct ChanOut<'g, 'a> { /// [`sunset_async::async_channel::ChanOut`] used in the context of an /// SFTP Session. /// +// TODO: complete this once the flow is fully developed /// The usage is simple: /// /// 1. SftpHandler will: Initialize the structure From 4e935079a318748160e707baeed91d72082202de Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 10 Nov 2025 10:53:12 +1100 Subject: [PATCH 337/393] [skip ci] WIP: Missing StatusCode field in Status derived from SSHEncode implementation for enum sshwire-derive lib.rs defines the rules to encode enums in `encode_enum` and the encoding decision is not to encode the value. See [implementation here](https://github.com/mkj/sunset/blob/8e5d20916cf7b29111b90e4d3b7bb7827c9be8e5/sshwire-derive/src/lib.rs#L287) This could be addressed by introducing a parameter for enum fields where the encoded data type is declared (or not). There is probably a good reason for this behavior. --- sftp/src/proto.rs | 37 +++++++++++++++++++++++++++++++++++++ sftp/src/sftpserver.rs | 36 ++++++++++++++++++------------------ sftp/src/sftpsink.rs | 1 + 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index a1bddeed..7fa4a62e 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -250,6 +250,7 @@ pub struct ResponseAttributes { pub struct ReqId(pub u32); /// For more information see [Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) +/// TODO: Reference! This is packed as u32 since that is the field data type in specs #[derive(Debug, FromPrimitive, SSHEncode)] #[repr(u32)] #[allow(non_camel_case_types, missing_docs)] @@ -794,3 +795,39 @@ sftpmessages! [ (104, Name, Name<'a>, "ssh_fxp_name"), }, ]; + +#[cfg(test)] +mod ProtoTests { + use super::*; + use crate::server::SftpSink; + + #[test] + fn test_status_encoding() { + let mut buf = [0u8; 256]; + let mut sink = SftpSink::new(&mut buf); + let status_packet = SftpPacket::Status( + ReqId(16), + Status { + code: StatusCode::SSH_FX_EOF, + message: "A".into(), + lang: "en-US".into(), + }, + ); + + let expected_status_packet_slice: [u8; 27] = [ + 0, 0, 0, 18, // Packet len + 101, // Packet type + 0, 0, 0, 16, // ReqId + 0, 0, 0, 4, // Status code + 0, 0, 0, 1, // string message length + 65, // string message content + 0, 0, 0, 5, // string lang length + 101, 110, 45, 85, 83, // string lang content + ]; + + let _ = status_packet.encode_response(&mut sink); + sink.finalize(); + + assert_eq!(&expected_status_packet_slice, sink.used_slice()); + } +} diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index a2038a6d..66e21924 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -138,6 +138,23 @@ pub struct ChanOut<'g, 'a> { _phantom_a: PhantomData<&'a ()>, // a' Why the second lifetime if ChanIO only needs one } +// TODO: complete this once the flow is fully developed +// `/ The usage is simple: +// / +// / 1. SftpHandler will: Initialize the structure +// / 2. The `SftpServer` trait implementation for 'readdir()' will: +// / +// / - Receive the DirReply ref 'reply' +// / +// / a. If there are items to send: +// / +// / - Instantiate a [`DirEntriesCollection`] with the items in the requested folder +// / - call the 'DirEntriesCollection.SendHeader(reply)' +// / - call the 'DirEntriesCollection.send_entries(reply)' +// / +// / b. If there are no items to send: +// / +// / - Call the 'reply.send_eof()' // TODO Define this /// Dir Reply is the structure that will be "visiting" the [`SftpServer`] /// trait @@ -148,25 +165,8 @@ pub struct ChanOut<'g, 'a> { /// [`sunset_async::async_channel::ChanOut`] used in the context of an /// SFTP Session. /// -// TODO: complete this once the flow is fully developed -/// The usage is simple: -/// -/// 1. SftpHandler will: Initialize the structure -/// 2. The `SftpServer` trait implementation for `readdir()` will: -/// -/// - Receive the DirReply ref `reply` -/// -/// a. If there are items to send: -/// -/// - Instantiate a [`DirEntriesCollection`] with the items in the requested folder -/// - call the `DirEntriesCollection.SendHeader(reply)` -/// - call the `DirEntriesCollection.send_entries(reply)` -/// -/// b. If there are no items to send: -/// -/// - Call the `reply.send_eof()` pub struct DirReply<'g, const N: usize> { - /// The request Id that will be used in the response + /// The request Id that will be use`d in the response req_id: ReqId, /// Immutable writer diff --git a/sftp/src/sftpsink.rs b/sftp/src/sftpsink.rs index 08fa7f9e..9fa221da 100644 --- a/sftp/src/sftpsink.rs +++ b/sftp/src/sftpsink.rs @@ -25,6 +25,7 @@ impl<'g> SftpSink<'g> { SftpSink { buffer: s, index: SFTP_FIELD_LEN_LENGTH } } + // TODO: Why don't you compute this every time that a new field is added? /// Finalise the buffer by prepending the packet length field, /// excluding the field itself. /// From 1d186ae56c8f91e33209429c8877147e61c78f48 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 12 Nov 2025 08:30:57 +1100 Subject: [PATCH 338/393] [skip ci] Fixed Status Encoding. Shall I extend the sshwire-derive to generate encoded values? To fix the issue with the message type encoding I have done a manual implementation of SSHEncode for `StatusCode`. It is tested in a proto_test mod. From last commit: sshwire-derive lib.rs defines the rules to encode enums in `encode_enum` and the encoding decision is not to encode the value. See [implementation here](https://github.com/mkj/sunset/blob/8e5d20916cf7b29111b90e4d3b7bb7827c9be8e5/sshwire-derive/src/lib.rs#L287) This could be addressed by introducing a parameter for enum fields where the encoded data type is declared (or not). There is probably a good reason for this behavior. --- sftp/src/proto.rs | 55 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 7fa4a62e..313f6ecd 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -166,6 +166,7 @@ pub struct Status<'a> { /// A language tag as defined by [Tags for the Identification of Languages](https://datatracker.ietf.org/doc/html/rfc1766) pub lang: TextString<'a>, } + /// Used for `ssh_fxp_handle` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7). #[derive(Debug, Clone, Copy, PartialEq, Eq, SSHEncode, SSHDecode)] pub struct Handle<'a> { @@ -251,29 +252,29 @@ pub struct ReqId(pub u32); /// For more information see [Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) /// TODO: Reference! This is packed as u32 since that is the field data type in specs -#[derive(Debug, FromPrimitive, SSHEncode)] +#[derive(Debug, FromPrimitive)] #[repr(u32)] #[allow(non_camel_case_types, missing_docs)] pub enum StatusCode { - #[sshwire(variant = "ssh_fx_ok")] + // #[sshwire(variant = "ssh_fx_ok")] SSH_FX_OK = 0, - #[sshwire(variant = "ssh_fx_eof")] + // #[sshwire(variant = "ssh_fx_eof")] SSH_FX_EOF = 1, - #[sshwire(variant = "ssh_fx_no_such_file")] + // #[sshwire(variant = "ssh_fx_no_such_file")] SSH_FX_NO_SUCH_FILE = 2, - #[sshwire(variant = "ssh_fx_permission_denied")] + // #[sshwire(variant = "ssh_fx_permission_denied")] SSH_FX_PERMISSION_DENIED = 3, - #[sshwire(variant = "ssh_fx_failure")] + // #[sshwire(variant = "ssh_fx_failure")] SSH_FX_FAILURE = 4, - #[sshwire(variant = "ssh_fx_bad_message")] + // #[sshwire(variant = "ssh_fx_bad_message")] SSH_FX_BAD_MESSAGE = 5, - #[sshwire(variant = "ssh_fx_no_connection")] + // #[sshwire(variant = "ssh_fx_no_connection")] SSH_FX_NO_CONNECTION = 6, - #[sshwire(variant = "ssh_fx_connection_lost")] + // #[sshwire(variant = "ssh_fx_connection_lost")] SSH_FX_CONNECTION_LOST = 7, - #[sshwire(variant = "ssh_fx_unsupported")] + // #[sshwire(variant = "ssh_fx_unsupported")] SSH_FX_OP_UNSUPPORTED = 8, - #[sshwire(unknown)] + // #[sshwire(unknown)] #[num_enum(catch_all)] Other(u32), } @@ -287,6 +288,32 @@ impl<'de> SSHDecode<'de> for StatusCode { } } +// TODO: Implement an automatic from implementation for u32 to Status code +// This is prone to errors if we update StatusCode enum +impl From<&StatusCode> for u32 { + fn from(value: &StatusCode) -> Self { + match value { + StatusCode::SSH_FX_OK => 0, + StatusCode::SSH_FX_EOF => 1, + StatusCode::SSH_FX_NO_SUCH_FILE => 2, + StatusCode::SSH_FX_PERMISSION_DENIED => 3, + StatusCode::SSH_FX_FAILURE => 4, + StatusCode::SSH_FX_BAD_MESSAGE => 5, + StatusCode::SSH_FX_NO_CONNECTION => 6, + StatusCode::SSH_FX_CONNECTION_LOST => 7, + StatusCode::SSH_FX_OP_UNSUPPORTED => 8, + StatusCode::Other(value) => *value, + } + } +} +// TODO: Implement an SSHEncode attribute for enums to encode them in a given numeric format +impl SSHEncode for StatusCode { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + let numeric_value: u32 = self.into(); + numeric_value.enc(s) + } +} + #[derive(Debug, SSHEncode, SSHDecode)] pub struct ExtPair<'a> { pub name: &'a str, @@ -797,7 +824,7 @@ sftpmessages! [ ]; #[cfg(test)] -mod ProtoTests { +mod proto_tests { use super::*; use crate::server::SftpSink; @@ -815,10 +842,10 @@ mod ProtoTests { ); let expected_status_packet_slice: [u8; 27] = [ - 0, 0, 0, 18, // Packet len + 0, 0, 0, 23, // Packet len 101, // Packet type 0, 0, 0, 16, // ReqId - 0, 0, 0, 4, // Status code + 0, 0, 0, 1, // Status code: SSH_FX_EOF 0, 0, 0, 1, // string message length 65, // string message content 0, 0, 0, 5, // string lang length From 5a35c7ee6b17dd033476223388572634c8c07a55 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 12 Nov 2025 08:57:46 +1100 Subject: [PATCH 339/393] [skip ci] WIP: First ls command successful Not ready yet. In order to answer EOF to a SSH_SXP_READDIR after all the dir items have been already sent. I am storing a `sftpserver::ReadStatus` passed as a result on `SftpServer.dirread()` with PendingData or EndOfFile. This would not work well in scenarios where there is more than one read transaction in course. I have decided that I am going to make the SftpServer implementators the responsibility to send the EOF status message but I will help them by providing a detailed trait documentation. To test this listing you may run the demo/sftp/std and run the script testing/test_read_dir.sh mind that the hard link counter is not recovered as it is not part of SFTP V3 implementation --- demo/sftp/std/src/demosftpserver.rs | 1 - demo/sftp/std/testing/test_read_dir.sh | 4 ++-- sftp/src/sftphandler/sftphandler.rs | 12 ++++++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index f7fde1cc..567ce906 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -304,7 +304,6 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { name_entry_collection.send_entries(reply).await?; - // name_entry_collection.send_eof(reply).await?; Ok(ReadStatus::EndOfFile) } else { error!("the path is not a directory = {:?}", dir_path); diff --git a/demo/sftp/std/testing/test_read_dir.sh b/demo/sftp/std/testing/test_read_dir.sh index 75d0dcb3..3f8d163c 100755 --- a/demo/sftp/std/testing/test_read_dir.sh +++ b/demo/sftp/std/testing/test_read_dir.sh @@ -22,9 +22,9 @@ ls echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." # Upload all files -sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'put ./%s\n' "${FILES[@]}") -ls +ls -lh bye EOF diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 230355a7..2914f4c7 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -515,6 +515,14 @@ where } } + /// Handles Healthy formed SftpRequest. Will return error if: + /// + /// - The request (SftpPacket) is not a request + /// + /// - The request is an unknown SftpPacket + /// + /// - The request is an initialization packet, and the initialization + /// has already been performed async fn handle_general_request( file_server: &mut S, last_read_status: &mut ReadStatus, @@ -658,10 +666,6 @@ where { Ok(read_status) => { *last_read_status = read_status; - // dir_reply should have sent a response - output_producer - .send_status(req_id, StatusCode::SSH_FX_EOF, "") - .await?; } Err(status) => { error!("Open failed: {:?}", status); From 3f4edd32011db8219381286909ee191f7c763bdb Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 12 Nov 2025 10:42:49 +1100 Subject: [PATCH 340/393] [skip ci] Implementing EOF responsibility in SftpServer and documenting --- demo/sftp/std/src/demofilehandlemanager.rs | 4 + demo/sftp/std/src/demosftpserver.rs | 40 +++++----- demo/sftp/std/testing/test_read_dir.sh | 3 + sftp/src/opaquefilehandle.rs | 3 + sftp/src/sftphandler/sftphandler.rs | 59 +++----------- sftp/src/sftpserver.rs | 91 +++++++++++++++++----- 6 files changed, 111 insertions(+), 89 deletions(-) diff --git a/demo/sftp/std/src/demofilehandlemanager.rs b/demo/sftp/std/src/demofilehandlemanager.rs index feaa126f..6b1cb278 100644 --- a/demo/sftp/std/src/demofilehandlemanager.rs +++ b/demo/sftp/std/src/demofilehandlemanager.rs @@ -56,4 +56,8 @@ where fn get_private_as_ref(&self, opaque_handle: &K) -> Option<&V> { self.handle_map.get(opaque_handle) } + + fn get_private_as_mut_ref(&mut self, opaque_handle: &K) -> Option<&mut V> { + self.handle_map.get_mut(opaque_handle) + } } diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 567ce906..8952afbb 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -36,6 +36,7 @@ pub(crate) struct PrivateFileHandle { #[derive(Debug)] pub(crate) struct PrivateDirHandle { path: String, + read_status: ReadStatus, } static OPAQUE_SALT: &'static str = "12d%32"; @@ -152,7 +153,10 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { debug!("Open Directory = {:?}", dir); let dir_handle = self.handles_manager.insert( - PrivatePathHandle::Directory(PrivateDirHandle { path: dir.into() }), + PrivatePathHandle::Directory(PrivateDirHandle { + path: dir.into(), + read_status: ReadStatus::default(), + }), OPAQUE_SALT, ); @@ -277,14 +281,21 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { &mut self, opaque_dir_handle: &DemoOpaqueFileHandle, reply: &DirReply<'_, N>, - ) -> SftpOpResult { + ) -> SftpOpResult<()> { debug!("read dir for {:?}", opaque_dir_handle); if let PrivatePathHandle::Directory(dir) = self .handles_manager - .get_private_as_ref(opaque_dir_handle) + .get_private_as_mut_ref(opaque_dir_handle) .ok_or(StatusCode::SSH_FX_NO_SUCH_FILE)? { + if dir.read_status == ReadStatus::EndOfFile { + reply.send_eof().await.map_err(|error| { + error!("{:?}", error); + StatusCode::SSH_FX_FAILURE + })?; + return Ok(()); + } let path_str = dir.path.clone(); debug!("opaque handle found in handles manager: {:?}", path_str); let dir_path = Path::new(&path_str); @@ -303,8 +314,8 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { name_entry_collection.send_entries_header(reply).await?; name_entry_collection.send_entries(reply).await?; - - Ok(ReadStatus::EndOfFile) + dir.read_status = ReadStatus::EndOfFile; + return Ok(()); } else { error!("the path is not a directory = {:?}", dir_path); return Err(StatusCode::SSH_FX_NO_SUCH_FILE); @@ -317,13 +328,11 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } // TODO Add this to SFTP library only available with std as a global helper -/// This is a helper structure to make ReadDir into something somehow -/// digestible by [`DirReply`] +/// This is a helper structure to make ReadDir into something manageable for +/// [`DirReply`] /// /// WIP: Not stable. It has know issues and most likely it's methods will change /// -/// BUG: It does not count properly the number of bytes -/// /// BUG: It does not include longname and that may be an issue #[derive(Debug)] pub struct DirEntriesCollection { @@ -337,9 +346,8 @@ pub struct DirEntriesCollection { impl DirEntriesCollection { pub fn new(dir_iterator: fs::ReadDir) -> Self { - let mut encoded_length = 9; // TODO We need to consider the packet type, Id and count fields - // This way I collect data required for the header and collect - // valid entries into a vector (only std) + let mut encoded_length = 0; + let entries: Vec = dir_iterator .filter_map(|entry_result| { let entry = entry_result.ok()?; @@ -421,13 +429,7 @@ impl DirEntriesCollection { attrs, }; debug!("Sending new item: {:?}", name_entry); - let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; - let mut sftp_sink = SftpSink::new(&mut buffer); - name_entry.enc(&mut sftp_sink).map_err(|err| { - error!("WireError: {:?}", err); - StatusCode::SSH_FX_FAILURE - })?; - reply.send_item(sftp_sink.payload_slice()).await.map_err(|err| { + reply.send_item(&name_entry).await.map_err(|err| { error!("SftpError: {:?}", err); StatusCode::SSH_FX_FAILURE })?; diff --git a/demo/sftp/std/testing/test_read_dir.sh b/demo/sftp/std/testing/test_read_dir.sh index 3f8d163c..29154da7 100755 --- a/demo/sftp/std/testing/test_read_dir.sh +++ b/demo/sftp/std/testing/test_read_dir.sh @@ -28,3 +28,6 @@ ls -lh bye EOF +echo "Cleaning up local files..." +rm -f -r ./*_random ./out/*_random + diff --git a/sftp/src/opaquefilehandle.rs b/sftp/src/opaquefilehandle.rs index 18f0662b..19450ef1 100644 --- a/sftp/src/opaquefilehandle.rs +++ b/sftp/src/opaquefilehandle.rs @@ -62,6 +62,9 @@ where /// Returns true if the opaque handle exist fn opaque_handle_exist(&self, opaque_handle: &K) -> bool; + /// given the opaque_handle returns a reference to the associated private handle + fn get_private_as_mut_ref(&mut self, opaque_handle: &K) -> Option<&mut V>; + /// given the opaque_handle returns a reference to the associated private handle fn get_private_as_ref(&self, opaque_handle: &K) -> Option<&V>; } diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 2914f4c7..ea70cdc9 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -107,13 +107,8 @@ where /// partially and the remaining is expected in successive buffers partial_write_request_tracker: Option>, + /// Used to handle received buffers that do not hold a complete request [`SftpPacket`] incomplete_request_holder: RequestHolder<'a>, - - /// Records the last read status reported - /// from a read operation. This is used to communicate - /// the EOF with the appropriate ReqId to a pending read operation - /// **WARNING**: This assumes that only one read operation is pending at a time. - last_read_status: ReadStatus, } impl<'a, T, S, const BUFFER_OUT_SIZE: usize> SftpHandler<'a, T, S, BUFFER_OUT_SIZE> @@ -138,7 +133,6 @@ where partial_write_request_tracker: None, state: SftpHandleState::default(), incomplete_request_holder: RequestHolder::new(incomplete_request_buffer), - last_read_status: ReadStatus::PendingData, } } @@ -238,7 +232,6 @@ where Ok(request) => { Self::handle_general_request( &mut self.file_server, - &mut self.last_read_status, output_producer, request, ) @@ -403,7 +396,6 @@ where Ok(request) => { Self::handle_general_request( &mut self.file_server, - &mut self.last_read_status, output_producer, request, ) @@ -525,7 +517,6 @@ where /// has already been performed async fn handle_general_request( file_server: &mut S, - last_read_status: &mut ReadStatus, output_producer: &SftpOutputProducer<'_, BUFFER_OUT_SIZE>, request: SftpPacket<'_>, ) -> Result<(), SftpError> @@ -632,48 +623,18 @@ where }; } SftpPacket::ReadDir(req_id, read_dir) => { - // TODO I should send back an EOF response when all the files in folder have been sent AND I have been asked for more files. - // According to https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7 - // This should be the file_server responsibility - - if (*last_read_status).eq(&ReadStatus::EndOfFile) { - let packet = SftpPacket::Status( - req_id, - Status { - code: StatusCode::SSH_FX_EOF, - message: "".into(), - lang: "en-US".into(), - }, - ); - let mut buf = [0u8; 256]; - let mut sink = SftpSink::new(&mut buf); - packet.encode_response(&mut sink)?; - debug!("Output Producer: Sending packet {:?}", packet); - sink.finalize(); - output_producer.send_data(sink.used_slice()).await?; - // output_producer - // .send_status(req_id, StatusCode::SSH_FX_EOF, "") - // .await?; - *last_read_status = ReadStatus::PendingData; - return Ok(()); - } - - let dir_reply = DirReply::new(req_id, output_producer); - - match file_server - .readdir(&T::try_from(&read_dir.handle)?, &dir_reply) + if let Err(status) = file_server + .readdir( + &T::try_from(&read_dir.handle)?, + &DirReply::new(req_id, output_producer), + ) .await { - Ok(read_status) => { - *last_read_status = read_status; - } - Err(status) => { - error!("Open failed: {:?}", status); + error!("Open failed: {:?}", status); - output_producer - .send_status(req_id, status, "Error Reading Directory") - .await?; - } + output_producer + .send_status(req_id, status, "Error Reading Directory") + .await?; }; } _ => { diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 66e21924..dbf489de 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,4 +1,5 @@ use crate::error::SftpResult; +use crate::proto::{MAX_NAME_ENTRY_SIZE, NameEntry}; use crate::server::SftpSink; use crate::sftphandler::SftpOutputProducer; use crate::{ @@ -7,7 +8,7 @@ use crate::{ }; use core::marker::PhantomData; -use log::{debug, trace}; +use log::{debug, error, trace}; use sunset::sshwire::SSHEncode; // use futures::executor::block_on; TODO Deal with the async nature of [`ChanOut`] @@ -15,8 +16,13 @@ use sunset::sshwire::SSHEncode; /// Result used to store the result of an Sftp Operation pub type SftpOpResult = core::result::Result; -/// Since the server needs to answer with an STATUS EOF to finish read requests, -/// Helps handling the completion for reading data. +/// To finish read requests the server needs to answer to +/// **subsequent READ requests** after all the data has been sent already +/// with a [`SftpPacket`] including a status code [`StatusCode::SSH_FX_EOF`]. +/// +/// [`ReadStatus`] enum has been implemented to keep record of these exhausted +/// read operations. +/// /// See: /// /// - [Reading and Writing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) @@ -24,10 +30,13 @@ pub type SftpOpResult = core::result::Result; #[derive(PartialEq, Debug, Default)] pub enum ReadStatus { // TODO Ideally this will contain an OwnedFileHandle - /// There is more data to read + /// There is more data to be read therefore the [`SftpServer`] will + /// send more data in the next read request. #[default] PendingData, - /// The server has provided all the data requested + /// The server has provided all the data requested therefore the [`SftpServer`] + /// will send a [`SftpPacket`] including a status code [`StatusCode::SSH_FX_EOF`] + /// in the next read request. EndOfFile, } @@ -91,13 +100,31 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - /// Reads the list of items in a directory + /// Reads the list of items in a directory and returns them using the [`DirReply`] + /// parameter. + /// + /// ## Notes to the implementer: + /// + /// The implementer is expected to use the parameter `reply` [`DirReply`] to: + /// + /// - In case of no more items in the directory to send, call `reply.send_eof()` + /// - There are more items in the directory: + /// 1. Call `reply.send_header()` with the number of items and the [`SSHEncode`] + /// length of all the items to be sent + /// 2. Call `reply.send_item()` for each of the items announced to be sent + /// 3. Do not call `reply.send_eof()` during this [`readdir`] method call + /// + /// The server is expected to keep track of the number of items that remain to be sent + /// to the client since the client will only stop asking for more elements in the + /// directory when a read dir request is answer with an reply.send_eof() + /// + #[allow(unused_variables)] async fn readdir( &mut self, opaque_dir_handle: &T, reply: &DirReply<'_, N>, - ) -> SftpOpResult { + ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", opaque_dir_handle @@ -156,10 +183,15 @@ pub struct ChanOut<'g, 'a> { // / // / - Call the 'reply.send_eof()' // TODO Define this -/// Dir Reply is the structure that will be "visiting" the [`SftpServer`] -/// trait -/// implementation via [`SftpServer::readdir()`] in order to send the -/// directory content list. + +/// Uses for [`DirReply`] to: +/// +/// - In case of no more items in the directory to be sent, call `reply.send_eof()` +/// - There are more items in the directory to be sent: +/// 1. Call `reply.send_header()` with the number of items and the [`SSHEncode`] +/// length of all the items to be sent +/// 2. Call `reply.send_item()` for each of the items announced to be sent +/// 3. Do not call `reply.send_eof()` during this [`readdir`] method call /// /// It handles immutable sending data via the underlying sftp-channel /// [`sunset_async::async_channel::ChanOut`] used in the context of an @@ -174,13 +206,19 @@ pub struct DirReply<'g, const N: usize> { } impl<'g, const N: usize> DirReply<'g, N> { - /// New instance - pub fn new(req_id: ReqId, chan_out: &'g SftpOutputProducer<'g, N>) -> Self { + /// New instances can only be created within the crate. Users can only + /// use other public methods to use it. + pub(crate) fn new( + req_id: ReqId, + chan_out: &'g SftpOutputProducer<'g, N>, + ) -> Self { // DirReply { chan_out: chan_out_wrapper, req_id } DirReply { req_id, chan_out } } - /// Sends the header to the client. TODO Make this enforceable + // TODO Make this enforceable + /// Sends the header to the client with the number of files as [`NameEntry`] and the [`SSHEncode`] + /// length of all these [`NameEntry`] items pub async fn send_header( &self, get_count: u32, @@ -194,9 +232,13 @@ impl<'g, const N: usize> DirReply<'g, N> { let mut sink = SftpSink::new(&mut s); get_encoded_len.enc(&mut sink)?; - 104u8.enc(&mut sink)?; // TODO Remove hack + 104u8.enc(&mut sink)?; // TODO Replace hack with self.req_id.enc(&mut sink)?; - get_count.enc(&mut sink)?; + let encoded_name_sftp_packet_length: u32 = 9; + // We need to consider the packet type, Id and count fields + // This way I collect data required for the header and collect + // valid entries into a vector (only std) + (get_count + encoded_name_sftp_packet_length).enc(&mut sink)?; let payload = sink.payload_slice(); debug!( "Sending header: len = {:?}, content = {:?}", @@ -207,11 +249,18 @@ impl<'g, const N: usize> DirReply<'g, N> { Ok(()) } - /// Sends an item to the client - pub async fn send_item(&self, data: &[u8]) -> SftpResult<()> { - debug!("Sending item: {:?} bytes", data.len()); - trace!("Sending item: content = {:?}", data); - self.chan_out.send_data(data).await + /// Sends a directory item to the client as a [`NameEntry`] + /// + /// Call this + pub async fn send_item(&self, name_entry: &NameEntry<'_>) -> SftpResult<()> { + let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; + let mut sftp_sink = SftpSink::new(&mut buffer); + name_entry.enc(&mut sftp_sink).map_err(|err| { + error!("WireError: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; + + self.chan_out.send_data(sftp_sink.payload_slice()).await } /// Sends EOF meaning that there is no more files in the directory From a5abc17f22c1ca6988aa9f00b2237bd3b3ae986e Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 12 Nov 2025 11:31:06 +1100 Subject: [PATCH 341/393] [skip ci] Added SunsetSftp Feature: Std Will be used for helpers only available to Std compilations --- demo/sftp/std/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/sftp/std/Cargo.toml b/demo/sftp/std/Cargo.toml index 1cce9708..8a3eaa5f 100644 --- a/demo/sftp/std/Cargo.toml +++ b/demo/sftp/std/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" sunset = { workspace = true, features = ["rsa", "std"] } sunset-async.workspace = true sunset-demo-common.workspace = true -sunset-sftp = { version = "0.1.0", path = "../../../sftp" } +sunset-sftp = { version = "0.1.0", path = "../../../sftp", features = ["std"] } # 131072 was determined empirically embassy-executor = { version = "0.7", features = [ From 6b3275bf5cf6f5a071375a5c36e4461760ad39aa Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 12 Nov 2025 11:33:18 +1100 Subject: [PATCH 342/393] [skip ci] Extracted DirEntriesCollection and added to std helpers This way we keep from std users some of the protocol details --- demo/sftp/std/src/demosftpserver.rs | 139 ++------------------------ sftp/Cargo.toml | 6 ++ sftp/src/lib.rs | 5 +- sftp/src/sftpserver.rs | 150 +++++++++++++++++++++++++++- 4 files changed, 164 insertions(+), 136 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 8952afbb..27ef3420 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -3,21 +3,16 @@ use crate::{ demoopaquefilehandle::DemoOpaqueFileHandle, }; -use sunset::sshwire::SSHEncode; use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; -use sunset_sftp::protocol::constants::MAX_NAME_ENTRY_SIZE; use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; +use sunset_sftp::server::helpers::DirEntriesCollection; use sunset_sftp::server::{ - DirReply, ReadReply, ReadStatus, SftpOpResult, SftpServer, SftpSink, + DirReply, ReadReply, ReadStatus, SftpOpResult, SftpServer, }; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; -use std::fs::DirEntry; -use std::os::linux::fs::MetadataExt; -use std::os::unix::fs::PermissionsExt; -use std::time::SystemTime; -use std::{fs, io}; +use std::fs; use std::{fs::File, os::unix::fs::FileExt, path::Path}; #[derive(Debug)] @@ -296,6 +291,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { })?; return Ok(()); } + let path_str = dir.path.clone(); debug!("opaque handle found in handles manager: {:?}", path_str); let dir_path = Path::new(&path_str); @@ -311,10 +307,10 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { let name_entry_collection = DirEntriesCollection::new(dir_iterator); - name_entry_collection.send_entries_header(reply).await?; + let response_read_status = + name_entry_collection.send_response(reply).await?; - name_entry_collection.send_entries(reply).await?; - dir.read_status = ReadStatus::EndOfFile; + dir.read_status = response_read_status; return Ok(()); } else { error!("the path is not a directory = {:?}", dir_path); @@ -326,124 +322,3 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } } } - -// TODO Add this to SFTP library only available with std as a global helper -/// This is a helper structure to make ReadDir into something manageable for -/// [`DirReply`] -/// -/// WIP: Not stable. It has know issues and most likely it's methods will change -/// -/// BUG: It does not include longname and that may be an issue -#[derive(Debug)] -pub struct DirEntriesCollection { - /// Number of elements - count: u32, - /// Computed length of all the encoded elements - encoded_length: u32, - /// The actual entries. As you can see these are DirEntry. This is a std choice - entries: Vec, -} - -impl DirEntriesCollection { - pub fn new(dir_iterator: fs::ReadDir) -> Self { - let mut encoded_length = 0; - - let entries: Vec = dir_iterator - .filter_map(|entry_result| { - let entry = entry_result.ok()?; - let filename = entry.file_name().to_string_lossy().into_owned(); - let name_entry = NameEntry { - filename: Filename::from(filename.as_str()), - _longname: Filename::from(""), - attrs: Self::get_attrs_or_empty(entry.metadata()), - }; - - let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; - let mut sftp_sink = SftpSink::new(&mut buffer); - name_entry.enc(&mut sftp_sink).ok()?; - //TODO remove this unchecked casting - encoded_length += sftp_sink.payload_len() as u32; - Some(entry) - }) - .collect(); - - //TODO remove this unchecked casting - let count = entries.len() as u32; - - info!( - "Processed {} entries, estimated serialized length: {}", - count, encoded_length - ); - - Self { count, encoded_length, entries } - } - - fn get_attrs_or_empty( - maybe_metadata: Result, - ) -> Attrs { - maybe_metadata.map(Self::get_attrs).unwrap_or_default() - } - - fn get_attrs(metadata: fs::Metadata) -> Attrs { - let time_to_u32 = |time_result: io::Result| { - time_result - .ok()? - .duration_since(SystemTime::UNIX_EPOCH) - .ok()? - .as_secs() - .try_into() - .ok() - }; - - Attrs { - size: Some(metadata.len()), - uid: Some(metadata.st_uid()), - gid: Some(metadata.st_gid()), - permissions: Some(metadata.permissions().mode()), - atime: time_to_u32(metadata.accessed()), - mtime: time_to_u32(metadata.modified()), - ext_count: None, - } - } - - pub async fn send_entries_header( - &self, - reply: &DirReply<'_, N>, - ) -> SftpOpResult<()> { - reply.send_header(self.count, self.encoded_length).await.map_err(|e| { - debug!("Could not send header {e:?}"); - StatusCode::SSH_FX_FAILURE - }) - } - - pub async fn send_entries( - &self, - reply: &DirReply<'_, N>, - ) -> SftpOpResult<()> { - for entry in &self.entries { - let filename = entry.file_name().to_string_lossy().into_owned(); - let attrs = Self::get_attrs_or_empty(entry.metadata()); - let name_entry = NameEntry { - filename: Filename::from(filename.as_str()), - _longname: Filename::from(""), - attrs, - }; - debug!("Sending new item: {:?}", name_entry); - reply.send_item(&name_entry).await.map_err(|err| { - error!("SftpError: {:?}", err); - StatusCode::SSH_FX_FAILURE - })?; - } - Ok(()) - } - - pub async fn no_files( - &self, - reply: &DirReply<'_, N>, - ) -> SftpOpResult<()> { - reply.send_eof().await.map_err(|err| { - error!("SftpError: {:?}", err); - StatusCode::SSH_FX_FAILURE - }) - } -} diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 86fb6d52..83acdde7 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -3,6 +3,12 @@ name = "sunset-sftp" version = "0.1.1" edition = "2024" +[features] +default = [] + +# Standard library support - enables std helpers +std = [] + [dependencies] sunset = { version = "0.3.0", path = "../" } sunset-async = { path = "../async", version = "0.3" } diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 751b0fa9..92451938 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -69,12 +69,15 @@ pub use sftphandler::SftpHandler; /// Structures and types used to add the details for the target system pub mod server { - // pub use crate::sftpserver::DirEntriesResponseHelpers; pub use crate::sftpserver::DirReply; pub use crate::sftpserver::ReadReply; pub use crate::sftpserver::ReadStatus; pub use crate::sftpserver::SftpOpResult; pub use crate::sftpserver::SftpServer; + #[cfg(feature = "std")] + pub mod helpers { + pub use crate::sftpserver::DirEntriesCollection; + } pub use crate::sftpsink::SftpSink; pub use sunset::sshwire::SSHEncode; } diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index dbf489de..a75b9772 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -7,11 +7,11 @@ use crate::{ proto::{Attrs, Name, ReqId, StatusCode}, }; -use core::marker::PhantomData; -use log::{debug, error, trace}; use sunset::sshwire::SSHEncode; -// use futures::executor::block_on; TODO Deal with the async nature of [`ChanOut`] +use core::marker::PhantomData; +#[allow(unused_imports)] +use log::{debug, error, info, log, trace, warn}; /// Result used to store the result of an Sftp Operation pub type SftpOpResult = core::result::Result; @@ -268,3 +268,147 @@ impl<'g, const N: usize> DirReply<'g, N> { self.chan_out.send_status(self.req_id, StatusCode::SSH_FX_EOF, "").await } } + +// TODO Add this to SFTP library only available with std as a global helper +#[cfg(feature = "std")] +use crate::proto::Filename; +#[cfg(feature = "std")] +use std::{ + fs::{DirEntry, Metadata, ReadDir}, + os::{linux::fs::MetadataExt, unix::fs::PermissionsExt}, + time::SystemTime, +}; + +#[cfg(feature = "std")] +/// This is a helper structure to make ReadDir into something manageable for +/// [`DirReply`] +/// +/// WIP: Not stable. It has know issues and most likely it's methods will change +/// +/// BUG: It does not include longname and that may be an issue +#[derive(Debug)] +pub struct DirEntriesCollection { + /// Number of elements + count: u32, + /// Computed length of all the encoded elements + encoded_length: u32, + /// The actual entries. As you can see these are DirEntry. This is a std choice + entries: Vec, +} + +#[cfg(feature = "std")] +impl DirEntriesCollection { + /// Creates this DirEntriesCollection so linux std users do not need to + /// translate `std` directory elements into Sftp structures before sending a response + /// back to the client + pub fn new(dir_iterator: ReadDir) -> Self { + use log::info; + + let mut encoded_length = 0; + + let entries: Vec = dir_iterator + .filter_map(|entry_result| { + let entry = entry_result.ok()?; + let filename = entry.file_name().to_string_lossy().into_owned(); + let name_entry = NameEntry { + filename: Filename::from(filename.as_str()), + _longname: Filename::from(""), + attrs: Self::get_attrs_or_empty(entry.metadata()), + }; + + let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; + let mut sftp_sink = SftpSink::new(&mut buffer); + name_entry.enc(&mut sftp_sink).ok()?; + //TODO remove this unchecked casting + encoded_length += sftp_sink.payload_len() as u32; + Some(entry) + }) + .collect(); + + //TODO remove this unchecked casting + let count = entries.len() as u32; + + info!( + "Processed {} entries, estimated serialized length: {}", + count, encoded_length + ); + + Self { count, encoded_length, entries } + } + + /// Using the provided [`DirReply`] sends a response taking care of + /// composing a SFTP Entry header and sending everything in the right order + /// + /// Returns a [`ReadStatus`] + pub async fn send_response( + &self, + reply: &DirReply<'_, N>, + ) -> SftpOpResult { + self.send_entries_header(reply).await?; + self.send_entries(reply).await?; + Ok(ReadStatus::EndOfFile) + } + /// Sends a header for all the elements in the ReadDir iterator + /// + /// It will take care of counting them and finding the serialized length of each + /// element + async fn send_entries_header( + &self, + reply: &DirReply<'_, N>, + ) -> SftpOpResult<()> { + reply.send_header(self.count, self.encoded_length).await.map_err(|e| { + debug!("Could not send header {e:?}"); + StatusCode::SSH_FX_FAILURE + }) + } + + /// Sends the entries in the ReadDir iterator back to the client + async fn send_entries( + &self, + reply: &DirReply<'_, N>, + ) -> SftpOpResult<()> { + for entry in &self.entries { + let filename = entry.file_name().to_string_lossy().into_owned(); + let attrs = Self::get_attrs_or_empty(entry.metadata()); + let name_entry = NameEntry { + filename: Filename::from(filename.as_str()), + _longname: Filename::from(""), + attrs, + }; + debug!("Sending new item: {:?}", name_entry); + reply.send_item(&name_entry).await.map_err(|err| { + error!("SftpError: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; + } + Ok(()) + } + + fn get_attrs_or_empty( + maybe_metadata: Result, + ) -> Attrs { + maybe_metadata.map(Self::get_attrs).unwrap_or_default() + } + + fn get_attrs(metadata: Metadata) -> Attrs { + let time_to_u32 = |time_result: std::io::Result| { + time_result + .ok()? + .duration_since(SystemTime::UNIX_EPOCH) + .ok()? + .as_secs() + .try_into() + .ok() + }; + + Attrs { + size: Some(metadata.len()), + uid: Some(metadata.st_uid()), + gid: Some(metadata.st_gid()), + permissions: Some(metadata.permissions().mode()), + atime: time_to_u32(metadata.accessed()), + mtime: time_to_u32(metadata.modified()), + ext_count: None, + } + } +} From 503dbafd3c1bb85376b1ef953869abeb2415c5d2 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 12 Nov 2025 14:37:06 +1100 Subject: [PATCH 343/393] [skip ci] Refactor proto.rs Name structure and sftpserver trait to remove the `std` dependency It splits the responsibility of encoding/decoding a SSH_FXP_NAME into Name and NameEntry<'a> avoiding the Vector or any fixed memory allocation for the list NameEntry. That required refactoring SftpServer.realpath() and SftpHandler::handle_general_request() to adopt this approach of sending a header and after that one or more NameEntry items Also documenting and reducing the number of warnings --- demo/sftp/std/src/demosftpserver.rs | 10 ++-- demo/sftp/std/src/main.rs | 2 +- sftp/src/lib.rs | 13 +++- sftp/src/proto.rs | 59 +++++++++++++------ sftp/src/sftphandler/sftphandler.rs | 25 ++++---- .../sftphandler/sftpoutputchannelhandler.rs | 22 ++----- sftp/src/sftpserver.rs | 43 ++++++++++---- 7 files changed, 109 insertions(+), 65 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 27ef3420..994626ed 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -163,9 +163,9 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { dir_handle } - fn realpath(&mut self, dir: &str) -> SftpOpResult> { - debug!("finding path for: {:?}", dir); - Ok(Name(vec![NameEntry { + fn realpath(&mut self, dir: &str) -> SftpOpResult> { + info!("finding path for: {:?}", dir); + let name_entry = NameEntry { filename: Filename::from(self.base_path.as_str()), _longname: Filename::from(""), attrs: Attrs { @@ -177,7 +177,9 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { mtime: None, ext_count: None, }, - }])) + }; + debug!("Will return: {:?}", name_entry); + Ok(name_entry) } fn close( diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index ff7cbd4c..eb716fed 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -208,7 +208,7 @@ async fn main(spawner: Spawner) { .filter_level(log::LevelFilter::Warn) .filter_module( "sunset_demo_sftp_std::demosftpserver", - log::LevelFilter::Info, + log::LevelFilter::Debug, ) .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Debug) .filter_module( diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 92451938..874e0b38 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -27,7 +27,7 @@ //! and [write](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) //! - [ ] File [read](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4), //! - [ ] File [stats](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8) -//! - [ ] Directory [Browsing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7) +//! - [x] Directory [Browsing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7) //! //! ## Minimal features for convenient usability //! @@ -49,7 +49,7 @@ #![forbid(unsafe_code)] #![warn(missing_docs)] -// #![no_std] +#![cfg_attr(not(feature = "std"), no_std)] mod opaquefilehandle; mod proto; @@ -67,6 +67,9 @@ mod sftpsource; pub use sftphandler::SftpHandler; /// Structures and types used to add the details for the target system +/// Related to the implementation of the [`server::SftpServer`], which +/// is meant to be instantiated by the user and passed to [`SftpHandler`] +/// and has the task of executing client requests in the underlying system pub mod server { pub use crate::sftpserver::DirReply; @@ -74,8 +77,12 @@ pub mod server { pub use crate::sftpserver::ReadStatus; pub use crate::sftpserver::SftpOpResult; pub use crate::sftpserver::SftpServer; - #[cfg(feature = "std")] + /// Helpers to reduce error prone tasks and hide some details that + /// add complexity when implementing an [`SftpServer`] pub mod helpers { + pub use crate::sftpserver::helpers::*; + + #[cfg(feature = "std")] pub use crate::sftpserver::DirEntriesCollection; } pub use crate::sftpsink::SftpSink; diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 313f6ecd..9c40a367 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -202,44 +202,65 @@ pub struct NameEntry<'a> { pub attrs: Attrs, } +/// This is the encoded length for the Name Sftp Response. +/// +/// This considers the Packet type (1), the Request Id (4) and +/// count of [`NameEntry`] that will follow +/// +/// It excludes the length of [`NameEntry`] explicitly +/// +/// It is defined a single source of truth for what is the length for the +/// encoded [`SftpPacket::Name`] variant +/// +/// See [Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) +pub(crate) const ENCODED_BASE_NAME_SFTP_PACKET_LENGTH: u32 = 9; + // TODO Will a Vector be an issue for no_std? // Maybe we should migrate this to heapless::Vec and let the user decide // the number of elements via features flags? +/// This is the first part of the `SSH_FXP_NAME` response. It includes +/// only the count of [`NameEntry`] items that follow this Name +/// +/// After encoding or decoding [`Name`], [`NameEntry`] must be encoded or +/// decoded `count` times /// A collection of [`NameEntry`] used for [ssh_fxp_name responses](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7). #[derive(Debug)] -pub struct Name<'a>(pub Vec>); +// pub struct Name<'a>(pub Vec>); +pub struct Name { + /// Number of [`NameEntry`] items that follow this Name + pub count: u32, +} -impl<'a: 'de, 'de> SSHDecode<'de> for Name<'a> -where - 'de: 'a, -{ +impl<'de> SSHDecode<'de> for Name { fn dec(s: &mut S) -> WireResult where S: SSHSource<'de>, { - let count = u32::dec(s)? as usize; + let count = u32::dec(s)? as u32; - let mut names = Vec::with_capacity(count); + // let mut names = Vec::with_capacity(count); - for _ in 0..count { - names.push(NameEntry::dec(s)?); - } + // for _ in 0..count { + // names.push(NameEntry::dec(s)?); + // } - Ok(Name(names)) + Ok(Name { count }) } } -impl<'a> SSHEncode for Name<'a> { +impl SSHEncode for Name { fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { - (self.0.len() as u32).enc(s)?; + self.count.enc(s) + // (self.0.len() as u32).enc(s)?; - for element in self.0.iter() { - element.enc(s)?; - } - Ok(()) + // for element in self.0.iter() { + // element.enc(s)?; + // } + // Ok(()) } } +// TODO: Is this really necessary? #[derive(Debug, SSHEncode, SSHDecode)] pub struct ResponseAttributes { pub attrs: Attrs, @@ -314,6 +335,8 @@ impl SSHEncode for StatusCode { } } +// TODO: Implement extensions. Low in priority +/// Provided to provide a mechanism to implement extensions #[derive(Debug, SSHEncode, SSHDecode)] pub struct ExtPair<'a> { pub name: &'a str, @@ -819,7 +842,7 @@ sftpmessages! [ (101, Status, Status<'a>, "ssh_fxp_status"), (102, Handle, Handle<'a>, "ssh_fxp_handle"), (103, Data, Data<'a>, "ssh_fxp_data"), - (104, Name, Name<'a>, "ssh_fxp_name"), + (104, Name, Name, "ssh_fxp_name"), }, ]; diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index ea70cdc9..aaa6e68f 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -4,10 +4,10 @@ use crate::error::SftpError; use crate::handles::OpaqueFileHandle; use crate::proto::{ self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, - SftpPacket, Status, StatusCode, + SftpPacket, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; -use crate::server::{DirReply, ReadStatus, SftpOpResult, SftpSink}; +use crate::server::DirReply; use crate::sftperror::SftpResult; use crate::sftphandler::sftpoutputchannelhandler::{ SftpOutputPipe, SftpOutputProducer, @@ -17,7 +17,7 @@ use crate::sftpsource::SftpSource; use embassy_futures::select::select; use sunset::Error as SunsetError; -use sunset::sshwire::{SSHEncode, SSHSource, WireError}; +use sunset::sshwire::{SSHSource, WireError}; use sunset_async::ChanInOut; use core::u32; @@ -493,6 +493,7 @@ where self.process(&buffer_in[0..lr], &output_producer).await?; } + #[allow(unreachable_code)] SftpResult::Ok(()) }; match select(processing_loop, output_consumer_loop).await { @@ -530,15 +531,15 @@ where return Err(SftpError::AlreadyInitialized); } SftpPacket::PathInfo(req_id, path_info) => { - let a_name = file_server.realpath(path_info.path.as_str()?)?; - - let response = SftpPacket::Name(req_id, a_name); - debug!( - "Request Id {:?}. Encoding response: {:?}", - &req_id, &response - ); - - output_producer.send_packet(&response).await?; + let dir_reply = DirReply::new(req_id, output_producer); + let name_entry = file_server.realpath(path_info.path.as_str()?)?; + + let encoded_len = + crate::sftpserver::helpers::get_name_entry_len(&name_entry)?; + debug!("PathInfo encoded length: {:?}", encoded_len); + trace!("PathInfo Response content: {:?}", encoded_len); + dir_reply.send_header(1, encoded_len).await?; + dir_reply.send_item(&name_entry).await?; } SftpPacket::Open(req_id, open) => { match file_server.open(open.filename.as_str()?, &open.attrs) { diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index ad2e8a30..967c24c7 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -74,14 +74,14 @@ impl<'a, const N: usize> SftpOutputConsumer<'a, N> { let mut buf = [0u8; N]; loop { let rl = self.reader.read(&mut buf).await; - let mut total = 0; + let mut _total = 0; { let mut lock = self.counter.lock().await; *lock += rl; - total = *lock; + _total = *lock; } - debug!("Output Consumer: Reads {rl} bytes. Total {total}"); + debug!("Output Consumer: Reads {rl} bytes. Total {_total}"); if rl > 0 { self.ssh_chan_out.write_all(&buf[..rl]).await?; debug!("Output Consumer: Written {:?} bytes ", &buf[..rl].len()); @@ -110,16 +110,6 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { Ok(()) } - /// Sends the data encoded in the provided [`SftpSink`] without including - /// the size. - /// - /// Use this when you are sending chunks of data after a valid header - pub async fn send_payload(&self, sftp_sink: &SftpSink<'_>) -> SftpResult<()> { - let buf = sftp_sink.payload_slice(); - Self::send_buffer(&self.writer, &buf, &self.counter).await; - Ok(()) - } - /// Simplifies the task of sending a status response to the client. pub async fn send_status( &self, @@ -153,14 +143,14 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { buf: &[u8], counter: &CounterMutex, ) { - let mut total = 0; + let mut _total = 0; { let mut lock = counter.lock().await; *lock += buf.len(); - total = *lock; + _total = *lock; } - debug!("Output Producer: Sends {:?} bytes. Total {total}", buf.len()); + debug!("Output Producer: Sends {:?} bytes. Total {_total}", buf.len()); trace!("Output Producer: Sending buffer {:?}", buf); // writer.write_all(buf); // ??? error[E0596]: cannot borrow `*writer` as mutable, as it is behind a `&` reference diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index a75b9772..5204c20d 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,10 +1,12 @@ use crate::error::SftpResult; -use crate::proto::{MAX_NAME_ENTRY_SIZE, NameEntry}; +use crate::proto::{ + ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, MAX_NAME_ENTRY_SIZE, NameEntry, +}; use crate::server::SftpSink; use crate::sftphandler::SftpOutputProducer; use crate::{ handles::OpaqueFileHandle, - proto::{Attrs, Name, ReqId, StatusCode}, + proto::{Attrs, ReqId, StatusCode}, }; use sunset::sshwire::SSHEncode; @@ -133,7 +135,7 @@ where } /// Provides the real path of the directory specified - fn realpath(&mut self, dir: &str) -> SftpOpResult> { + fn realpath(&mut self, dir: &str) -> SftpOpResult> { log::error!("SftpServer RealPath operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } @@ -206,6 +208,8 @@ pub struct DirReply<'g, const N: usize> { } impl<'g, const N: usize> DirReply<'g, N> { + // const ENCODED_NAME_SFTP_PACKET_LENGTH: u32 = 9; + /// New instances can only be created within the crate. Users can only /// use other public methods to use it. pub(crate) fn new( @@ -221,24 +225,23 @@ impl<'g, const N: usize> DirReply<'g, N> { /// length of all these [`NameEntry`] items pub async fn send_header( &self, - get_count: u32, - get_encoded_len: u32, + count: u32, + items_encoded_len: u32, ) -> SftpResult<()> { debug!( "I will send the header here for request id {:?}: count = {:?}, length = {:?}", - self.req_id, get_count, get_encoded_len + self.req_id, count, items_encoded_len ); let mut s = [0u8; N]; let mut sink = SftpSink::new(&mut s); - get_encoded_len.enc(&mut sink)?; - 104u8.enc(&mut sink)?; // TODO Replace hack with - self.req_id.enc(&mut sink)?; - let encoded_name_sftp_packet_length: u32 = 9; // We need to consider the packet type, Id and count fields // This way I collect data required for the header and collect // valid entries into a vector (only std) - (get_count + encoded_name_sftp_packet_length).enc(&mut sink)?; + (items_encoded_len + ENCODED_BASE_NAME_SFTP_PACKET_LENGTH).enc(&mut sink)?; + 104u8.enc(&mut sink)?; // TODO Replace hack with + self.req_id.enc(&mut sink)?; + count.enc(&mut sink)?; let payload = sink.payload_slice(); debug!( "Sending header: len = {:?}, content = {:?}", @@ -269,6 +272,24 @@ impl<'g, const N: usize> DirReply<'g, N> { } } +pub mod helpers { + use crate::{ + error::SftpResult, + proto::{MAX_NAME_ENTRY_SIZE, NameEntry}, + server::SftpSink, + }; + + use sunset::sshwire::SSHEncode; + + /// Helper function to get the length of a [`NameEntry`] + pub fn get_name_entry_len(name_entry: &NameEntry<'_>) -> SftpResult { + let mut buf = [0u8; MAX_NAME_ENTRY_SIZE]; + let mut temp_sink = SftpSink::new(&mut buf); + name_entry.enc(&mut temp_sink)?; + Ok(temp_sink.payload_len() as u32) + } +} + // TODO Add this to SFTP library only available with std as a global helper #[cfg(feature = "std")] use crate::proto::Filename; From 44c21cdaf13823205bbe843a1f37097a5076271d Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 12 Nov 2025 15:03:45 +1100 Subject: [PATCH 344/393] Basic Features Directory read is now complete sunset-sftp and sunset-demo-sftp-std crates version have been increased and now match. Documentation has been updated, An Producer Consumer subsystem has been introduced to allow sending responses exceeding the output buffer length without introducing lifetimes and borrowing issues. proto.rs has been refactored to eliminate all std dependencies. helper structures and modules have been added to assist the SftpServer` implementer. a new crate feature `std` has been introduced to allow accessing std helper structures that reduce the exposure of sftp protocol details to the `SftpServer` implementer. The std example has been refactored. While still quite complex it delegates some pretty specific tasks to the std helpers. --- demo/sftp/std/Cargo.toml | 2 +- sftp/Cargo.toml | 2 +- sftp/src/lib.rs | 2 +- sftp/src/sftphandler/sftphandler.rs | 5 +++++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/demo/sftp/std/Cargo.toml b/demo/sftp/std/Cargo.toml index 8a3eaa5f..a2f08d6f 100644 --- a/demo/sftp/std/Cargo.toml +++ b/demo/sftp/std/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sunset-demo-sftp-std" -version = "0.1.0" +version = "0.1.2" edition = "2021" [dependencies] diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 83acdde7..704a9a84 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sunset-sftp" -version = "0.1.1" +version = "0.1.2" edition = "2024" [features] diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 874e0b38..6f518fd1 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -25,9 +25,9 @@ //! - [x] [Canonicalizing the Server-Side Path Name](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.11) support //! - [x] [Open, close](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3) //! and [write](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) +//! - [x] Directory [Browsing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7) //! - [ ] File [read](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4), //! - [ ] File [stats](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8) -//! - [x] Directory [Browsing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7) //! //! ## Minimal features for convenient usability //! diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index aaa6e68f..42a91c39 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -91,6 +91,11 @@ enum FragmentedRequestState { /// It will delegate request to an [`crate::sftpserver::SftpServer`] /// implemented by the library /// user taking into account the local system details. +/// +/// The compiler time constant `BUFFER_OUT_SIZE` is used to define the +/// size of the output buffer for the subsystem [`Embassy-sync::pipe`] used +/// to send responses safely across the instantiated structure. +/// pub struct SftpHandler<'a, T, S, const BUFFER_OUT_SIZE: usize> where T: OpaqueFileHandle, From 91fc26af11ad6974b5f5291fc8fae7a7102ec983 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 09:41:48 +1100 Subject: [PATCH 345/393] [skip ci] Extending SftpSource and Tests Now we can peak the total packet length and also if the contained packet fits within the buffer. Tests for all of that --- demo/sftp/std/src/main.rs | 2 +- sftp/src/proto.rs | 17 +++-- sftp/src/sftpsource.rs | 134 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 142 insertions(+), 11 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index eb716fed..7ddf09a8 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -205,7 +205,7 @@ async fn listen( #[embassy_executor::main] async fn main(spawner: Spawner) { env_logger::builder() - .filter_level(log::LevelFilter::Warn) + .filter_level(log::LevelFilter::Debug) .filter_module( "sunset_demo_sftp_std::demosftpserver", log::LevelFilter::Debug, diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 9c40a367..09cbbff2 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -13,17 +13,26 @@ use paste::paste; #[allow(unused)] pub const SFTP_MINIMUM_PACKET_LEN: usize = 9; +#[allow(unused)] +pub const SFTP_FIELD_LEN_INDEX: usize = 0; +/// SFTP packets length field us u32 +#[allow(unused)] +pub const SFTP_FIELD_LEN_LENGTH: usize = 4; /// SFTP packets have the packet type after a u32 length field #[allow(unused)] pub const SFTP_FIELD_ID_INDEX: usize = 4; /// SFTP packets ID length is 1 byte -// pub const SFTP_FIELD_ID_LEN: usize = 1; +#[allow(unused)] +pub const SFTP_FIELD_ID_LEN: usize = 1; /// SFTP packets start with the length field + +/// SFTP packets have the packet request id after field id #[allow(unused)] -pub const SFTP_FIELD_LEN_INDEX: usize = 0; -/// SFTP packets length field us u32 +pub const SFTP_FIELD_REQ_ID_INDEX: usize = 5; +/// SFTP packets ID length is 1 byte #[allow(unused)] -pub const SFTP_FIELD_LEN_LENGTH: usize = 4; +pub const SFTP_FIELD_REQ_ID_LEN: usize = 4; +/// SFTP packets start with the length field // SSH_FXP_WRITE SFTP Packet definition used to decode long packets that do not fit in one buffer diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 1a29a149..0266dbd6 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -2,7 +2,8 @@ use crate::error::{SftpError, SftpResult}; use crate::handles::OpaqueFileHandle; use crate::proto::{ ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, - SFTP_MINIMUM_PACKET_LEN, SFTP_WRITE_REQID_INDEX, SftpNum, + SFTP_FIELD_REQ_ID_INDEX, SFTP_FIELD_REQ_ID_LEN, SFTP_MINIMUM_PACKET_LEN, + SFTP_WRITE_REQID_INDEX, SftpNum, }; use crate::protocol::FileHandle; use crate::sftphandler::PartialWriteRequestTracker; @@ -50,7 +51,6 @@ impl<'de> SftpSource<'de> { debug!("New source with content: : {:?}", buffer); SftpSource { buffer: buffer, index: 0 } } - /// Peaks the buffer for packet type [`SftpNum`]. This does not advance /// the reading index /// @@ -58,7 +58,7 @@ impl<'de> SftpSource<'de> { /// `dec(s)` would fail /// /// **Warning**: will only work in well formed packets, in other case - /// the result will contain garbage + /// the result will contains garbage pub(crate) fn peak_packet_type(&self) -> WireResult { if self.buffer.len() <= SFTP_FIELD_ID_INDEX { error!( @@ -72,13 +72,16 @@ impl<'de> SftpSource<'de> { } } - /// Peaks the buffer for packet length. This does not advance the reading index + /// Peaks the buffer for packet length field. This does not advance the reading index /// /// Useful to observe the packet fields in special conditions where a `dec(s)` /// would fail /// + /// Use `peak_total_packet_len` instead if you want to also consider the the + /// length field + /// /// **Warning**: will only work in well formed packets, in other case the result - /// will contain garbage + /// will contains garbage pub(crate) fn peak_packet_len(&self) -> WireResult { if self.buffer.len() < SFTP_FIELD_LEN_INDEX + SFTP_FIELD_LEN_LENGTH { Err(WireError::RanOut) @@ -92,6 +95,32 @@ impl<'de> SftpSource<'de> { } } + /// Peaks the packet in the source to obtain a total packet length, which + /// considers the length of the length field itself. For the packet length field + /// use [`peak_packet_len()`] + /// + /// This does not advance the reading index + /// + /// This does not consider the length field itself + /// Useful to observe the packet fields in special conditions where a `dec(s)` + /// would fail + /// + /// **Warning**: will only work in well formed packets, in other case the result + /// will contains garbage + pub(crate) fn peak_total_packet_len(&self) -> WireResult { + Ok(self.peak_packet_len()? + SFTP_FIELD_LEN_LENGTH as u32) + } + + // TODO: Test This for correctness + /// Compares the total source capacity and the peaked packet length + /// plus the length field length itself to find out if the packet fit + /// in the source + /// **Warning**: will only work in well formed packets, in other case + /// the result will contains garbage + pub fn packet_fits(&self) -> WireResult { + Ok(self.buffer.len() >= self.peak_total_packet_len()? as usize) + } + /// Assuming that the buffer contains a [`proto::Write`] request packet initial /// bytes and not its totality: /// @@ -104,7 +133,7 @@ impl<'de> SftpSource<'de> { /// - [`PartialWriteRequestTracker`] to handle subsequent portions of the request /// /// **Warning**: will only work in well formed write packets, in other case - /// the result will contain garbage + /// the result will contains garbage pub(crate) fn dec_packet_partial_write_content_and_get_tracker< T: OpaqueFileHandle, >( @@ -159,4 +188,97 @@ impl<'de> SftpSource<'de> { ) -> WireResult> { Ok(BinString(self.take(len)?)) } + + pub fn peak_packet_req_id(&self) -> WireResult { + if self.buffer.len() < SFTP_FIELD_REQ_ID_INDEX + SFTP_FIELD_REQ_ID_LEN { + Err(WireError::RanOut) + } else { + let bytes: [u8; 4] = self.buffer[SFTP_FIELD_REQ_ID_INDEX + ..SFTP_FIELD_REQ_ID_INDEX + SFTP_FIELD_LEN_LENGTH] + .try_into() + .expect("slice length mismatch"); + + Ok(u32::from_be_bytes(bytes)) + } + } + + /// Discards the first elements of the + pub fn consume_first(&mut self, len: usize) -> WireResult<()> { + if len > self.buffer.len() { + Err(WireError::RanOut) + } else { + self.index = len; + Ok(()) + } + } +} + +#[cfg(test)] +mod local_tests { + use super::*; + + fn status_buffer() -> [u8; 27] { + let expected_status_packet_slice: [u8; 27] = [ + 0, 0, 0, 23, // Packet len + 101, // Packet type + 0, 0, 0, 16, // ReqId + 0, 0, 0, 1, // Status code: SSH_FX_EOF + 0, 0, 0, 1, // string message length + 65, // string message content + 0, 0, 0, 5, // string lang length + 101, 110, 45, 85, 83, // string lang content + ]; + expected_status_packet_slice + } + + #[test] + fn peaking_len() { + let buffer_status = status_buffer(); + let sink = SftpSource::new(&buffer_status); + + let read_packet_len = sink.peak_packet_len().unwrap(); + let original_packet_len = 23u32; + assert_eq!(original_packet_len, read_packet_len); + } + #[test] + fn peaking_total_len() { + let buffer_status = status_buffer(); + let sink = SftpSource::new(&buffer_status); + + let read_total_packet_len = sink.peak_total_packet_len().unwrap(); + let original_total_packet_len = 23u32 + 4u32; + assert_eq!(original_total_packet_len, read_total_packet_len); + } + + #[test] + fn peaking_type() { + let buffer_status = status_buffer(); + let sink = SftpSource::new(&buffer_status); + let read_packet_type = sink.peak_packet_type().unwrap(); + let original_packet_type = SftpNum::from(101u8); + assert_eq!(original_packet_type, read_packet_type); + } + #[test] + fn peaking_req_id() { + let buffer_status = status_buffer(); + let sink = SftpSource::new(&buffer_status); + let read_req_id = sink.peak_packet_req_id().unwrap(); + let original_req_id = 16u32; + assert_eq!(original_req_id, read_req_id); + } + + #[test] + fn packet_does_fit() { + let buffer_status = status_buffer(); + let sink = SftpSource::new(&buffer_status); + assert_eq!(true, sink.packet_fits().unwrap()); + } + + #[test] + fn packet_does_not_fit() { + let buffer_status = status_buffer(); + let no_room_buffer = &buffer_status[..buffer_status.len() - 2]; + let sink = SftpSource::new(no_room_buffer); + assert_eq!(false, sink.packet_fits().unwrap()); + } } From 98094333927a9d74d329e31cd01273aa4e520301 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 09:48:38 +1100 Subject: [PATCH 346/393] [skip ci] SftpOutputPipe checks if already splits --- sftp/src/sftphandler/sftphandler.rs | 35 +------------------ .../sftphandler/sftpoutputchannelhandler.rs | 14 +++++--- 2 files changed, 11 insertions(+), 38 deletions(-) diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 42a91c39..e5e2f7d9 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -52,39 +52,6 @@ enum FragmentedRequestState { ProcessingLongRequest, } -// // TODO Generalize this to allow other request types -// /// Used to keep record of a long SFTP Write request that does not fit in -// /// receiving buffer and requires processing in batches -// #[derive(Debug)] -// pub struct PartialWriteRequestTracker { -// req_id: ReqId, -// opaque_handle: T, -// remain_data_len: u32, -// remain_data_offset: u64, -// } - -// impl PartialWriteRequestTracker { -// /// Creates a new [`PartialWriteRequestTracker`] -// pub fn new( -// req_id: ReqId, -// opaque_handle: T, -// remain_data_len: u32, -// remain_data_offset: u64, -// ) -> WireResult { -// Ok(PartialWriteRequestTracker { -// req_id, -// opaque_handle: opaque_handle, -// remain_data_len, -// remain_data_offset, -// }) -// } -// /// Returns the opaque file handle associated with the request -// /// tracked -// pub fn get_opaque_file_handle(&self) -> T { -// self.opaque_handle.clone() -// } -// } - /// Process the raw buffers in and out from a subsystem channel decoding /// request and encoding responses /// @@ -481,7 +448,7 @@ where let mut sftp_output_pipe = SftpOutputPipe::::new(); let (mut output_consumer, output_producer) = - sftp_output_pipe.split(chan_out); + sftp_output_pipe.split(chan_out)?; let output_consumer_loop = output_consumer.receive_task(); diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 967c24c7..85d051f8 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -1,4 +1,4 @@ -use crate::error::SftpResult; +use crate::error::{SftpError, SftpResult}; use crate::proto::{ReqId, SftpPacket, Status, StatusCode}; use crate::server::SftpSink; @@ -17,6 +17,7 @@ pub struct SftpOutputPipe { pipe: Pipe, counter_send: CounterMutex, counter_recv: CounterMutex, + splitted: bool, } /// M: SunsetSunsetRawMutex @@ -33,6 +34,7 @@ impl SftpOutputPipe { pipe: Pipe::new(), counter_send: Mutex::::new(0), counter_recv: Mutex::::new(0), + splitted: false, } } @@ -50,12 +52,16 @@ impl SftpOutputPipe { pub fn split<'a>( &'a mut self, ssh_chan_out: ChanOut<'a>, - ) -> (SftpOutputConsumer<'a, N>, SftpOutputProducer<'a, N>) { + ) -> SftpResult<(SftpOutputConsumer<'a, N>, SftpOutputProducer<'a, N>)> { + if self.splitted { + return Err(SftpError::AlreadyInitialized); + } + self.splitted = true; let (reader, writer) = self.pipe.split(); - ( + Ok(( SftpOutputConsumer { reader, ssh_chan_out, counter: &self.counter_recv }, SftpOutputProducer { writer, counter: &self.counter_send }, - ) + )) } } From add80145ce0abb5cfbbcbe1416fbbc8f0639b025 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 09:49:28 +1100 Subject: [PATCH 347/393] removed TODO, not a good idea --- sftp/src/sftphandler/partialwriterequesttracker.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftp/src/sftphandler/partialwriterequesttracker.rs b/sftp/src/sftphandler/partialwriterequesttracker.rs index 72acbfe4..38ea4f6a 100644 --- a/sftp/src/sftphandler/partialwriterequesttracker.rs +++ b/sftp/src/sftphandler/partialwriterequesttracker.rs @@ -55,6 +55,6 @@ impl PartialWriteRequestTracker { } pub(crate) fn get_req_id(&self) -> ReqId { - self.req_id.clone() // TODO reference? + self.req_id.clone() } } From 7bc1a634d80102bdad1b04428971c441fcc24997 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 10:08:19 +1100 Subject: [PATCH 348/393] [skip ci] intercepting incomplete UnknownPacket in decode request to help flushing it It is important that we flush the rest of an incomplete unknown packet. So until it is completed, we will return a RanOut so we can read it full and then flush it --- sftp/src/proto.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 09cbbff2..9dd5fa17 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1,3 +1,5 @@ +use crate::sftpsource::SftpSource; + use sunset::sshwire::{ BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, TextString, WireError, WireResult, @@ -277,7 +279,7 @@ pub struct ResponseAttributes { // Requests/Responses data types -#[derive(Debug, SSHEncode, SSHDecode, Clone, Copy)] +#[derive(Debug, SSHEncode, SSHDecode, Clone, Copy, PartialEq)] pub struct ReqId(pub u32); /// For more information see [Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) @@ -508,7 +510,7 @@ macro_rules! sftpmessages { ) => { paste! { /// Represent a subset of the SFTP packet types defined by draft-ietf-secsh-filexfer-02 - #[derive(Debug, Clone, FromPrimitive, SSHEncode)] + #[derive(Debug, Clone, PartialEq, FromPrimitive, SSHEncode)] #[repr(u8)] #[allow(non_camel_case_types)] pub enum SftpNum { @@ -729,9 +731,9 @@ macro_rules! sftpmessages { /// Decode a response. /// /// Used by a SFTP client. Does not include the length field. - pub fn decode_response<'de, S>(s: &mut S) -> WireResult<(ReqId, Self)> + pub fn decode_response<'de>(s: &mut SftpSource<'de>) -> WireResult<(ReqId, Self)> where - S: SSHSource<'de>, + // S: SftpSource<'de>, 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes 'de: 'a { @@ -753,9 +755,9 @@ macro_rules! sftpmessages { /// Used by a SFTP server. Does not include the length field. /// /// It will fail if the received packet is a response, no valid or incomplete packet - pub fn decode_request<'de, S>(s: &mut S) -> WireResult + pub fn decode_request<'de>(s: &mut SftpSource<'de>) -> WireResult where - S: SSHSource<'de>, + // S: SftpSource<'de>, 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes 'de: 'a { @@ -774,7 +776,11 @@ macro_rules! sftpmessages { } }, Err(e) => { - Err(e) + match e { + WireError::UnknownPacket{..} if !s.packet_fits()? => Err(WireError::RanOut), + _ => Err(e) + } + } } } From c1d446a348934eb16654ee55230ac6066974c85a Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 10:11:26 +1100 Subject: [PATCH 349/393] Handling properly unknown packets This way: - We send the right ReqId for Unsupported status - Flush out the complete packet to avoid desynchronizing packet decoding --- sftp/src/sftphandler/sftphandler.rs | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index e5e2f7d9..fc94392d 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -212,6 +212,25 @@ where self.state = SftpHandleState::Idle; } Err(e) => match e { + WireError::UnknownPacket { number } => { + warn!( + "Unknown packet: packetId = {:?}. Will flush \ + its length and send unsupported back", + number + ); + + let req_id = ReqId(source.peak_packet_req_id()?); + let len = + source.peak_total_packet_len()? as usize; + source.consume_first(len)?; + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_OP_UNSUPPORTED, + "Error decoding SFTP Packet", + ) + .await?; + } WireError::RanOut => match Self::handle_ran_out( &mut self.file_server, output_producer, @@ -413,6 +432,36 @@ where } }; } + WireError::UnknownPacket { number } => { + warn!( + "Unknown packet: packetId = {:?}. Will flush \ + its length and send unsupported back", + number + ); + /* TODO: The packet is unknown, but we are not consuming it properly. + That has the side effect that we will be interpreting chunks of that packet + as new packets and sending more garbage back to the client + Will result on an Close and not simply a clean message saying that + - Peek out the ReqId + - Peek out the length + + - flush the packet len if possible (do we have the whole length?) + + - Send an StatusCode with the relevant ReqId + + - Move on! + */ + let req_id = ReqId(source.peak_packet_req_id()?); + let len = source.peak_total_packet_len()? as usize; + source.consume_first(len)?; + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_OP_UNSUPPORTED, + "Error decoding SFTP Packet", + ) + .await?; + } _ => { error!("Error decoding SFTP Packet: {:?}", e); output_producer From 101aa8fad403132ba294a01db1153457fb43c124 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 10:11:49 +1100 Subject: [PATCH 350/393] One more time I missed the Cargo.lock changes --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 500e36e1..0176d4c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2851,7 +2851,7 @@ dependencies = [ [[package]] name = "sunset-demo-sftp-std" -version = "0.1.0" +version = "0.1.2" dependencies = [ "async-io", "critical-section", @@ -2915,7 +2915,7 @@ dependencies = [ [[package]] name = "sunset-sftp" -version = "0.1.1" +version = "0.1.2" dependencies = [ "embassy-futures", "embassy-sync 0.7.2", From 8c7de5ce6c0c621e7d0e21655bbebab11a478a1e Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 10:13:11 +1100 Subject: [PATCH 351/393] Adding test stats: For now I get an expected `Operation unsupported` --- demo/sftp/std/testing/test_stats.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100755 demo/sftp/std/testing/test_stats.sh diff --git a/demo/sftp/std/testing/test_stats.sh b/demo/sftp/std/testing/test_stats.sh new file mode 100755 index 00000000..f079fe3d --- /dev/null +++ b/demo/sftp/std/testing/test_stats.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +echo "Testing Stats..." + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Define test files +FILES=("512B_random") + +# Generate random data files +echo "Generating random data files..." +dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null + +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." + +# Upload all files +sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +$(printf 'put ./%s\n' "${FILES[@]}") +$(printf 'ls -lh ./%s\n' "${FILES[@]}") + +bye +EOF + +echo "Cleaning up local files..." +rm -f -r ./*_random ./out/*_random From 8961f7125b4e943c58eeae2021baa87171878785 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 11:44:05 +1100 Subject: [PATCH 352/393] refactored std helper to expose get_file_attrs --- demo/sftp/std/src/demosftpserver.rs | 15 +++++++++++++++ sftp/src/lib.rs | 1 + 2 files changed, 16 insertions(+) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 994626ed..72193b82 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -323,4 +323,19 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { return Err(StatusCode::SSH_FX_NO_SUCH_FILE); } } + + fn liststats(&mut self, file_path: &str) -> SftpOpResult { + log::debug!("SftpServer ListStats: file_path = {:?}", file_path); + let file_path = Path::new(file_path); + if file_path.is_file() { + return Ok(sunset_sftp::server::helpers::get_file_attrs( + file_path.metadata().map_err(|err| { + error!("Problem listing stats: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?, + )); + } else { + return Err(StatusCode::SSH_FX_NO_SUCH_FILE); + } + } } diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 6f518fd1..06b56d4a 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -84,6 +84,7 @@ pub mod server { #[cfg(feature = "std")] pub use crate::sftpserver::DirEntriesCollection; + pub use crate::sftpserver::get_file_attrs; } pub use crate::sftpsink::SftpSink; pub use sunset::sshwire::SSHEncode; From 53f4bac637b45ab67f8a55d1d9690b8570486422 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 11:45:12 +1100 Subject: [PATCH 353/393] completed getting file stats Tested with `test_get.sh` Ticked off from the roadmap --- demo/sftp/std/testing/test_get.sh | 27 ++++++++ sftp/src/lib.rs | 2 +- sftp/src/proto.rs | 10 +++ sftp/src/sftphandler/sftphandler.rs | 21 ++++++- sftp/src/sftpserver.rs | 95 ++++++++++++++++------------- 5 files changed, 111 insertions(+), 44 deletions(-) create mode 100755 demo/sftp/std/testing/test_get.sh diff --git a/demo/sftp/std/testing/test_get.sh b/demo/sftp/std/testing/test_get.sh new file mode 100755 index 00000000..e191895f --- /dev/null +++ b/demo/sftp/std/testing/test_get.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +echo "Testing Stats..." + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Define test files +FILES=("512B_random") + +# Generate random data files +echo "Generating random data files..." +dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null + +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." + +# Upload all files +sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +$(printf 'put ./%s\n' "${FILES[@]}") +$(printf 'get ./%s\n' "${FILES[@]}") + +bye +EOF + +echo "Cleaning up local files..." +rm -f -r ./*_random ./out/*_random diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 06b56d4a..9050cbb1 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -27,7 +27,7 @@ //! and [write](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) //! - [x] Directory [Browsing](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7) //! - [ ] File [read](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4), -//! - [ ] File [stats](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8) +//! - [x] File [stats](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8) //! //! ## Minimal features for convenient usability //! diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 9dd5fa17..6fab4e74 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -158,6 +158,14 @@ pub struct Write<'a> { pub data: BinString<'a>, } +/// Used for `ssh_fxp_lstat` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8). +/// LSTAT does not follow symbolic links +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct LStat<'a> { + /// The path of the element which stats are to be retrieved + pub file_path: TextString<'a>, +} + // ============================= Responses ============================= /// Used for `ssh_fxp_realpath` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.11). @@ -848,6 +856,7 @@ sftpmessages! [ (4, Close, Close<'a>, "ssh_fxp_close"), (5, Read, Read<'a>, "ssh_fxp_read"), (6, Write, Write<'a>, "ssh_fxp_write"), + (7, LStat, LStat<'a>, "ssh_fxp_lstat"), (11, OpenDir, OpenDir<'a>, "ssh_fxp_opendir"), (12, ReadDir, ReadDir<'a>, "ssh_fxp_readdir"), (16, PathInfo, PathInfo<'a>, "ssh_fxp_realpath"), @@ -858,6 +867,7 @@ sftpmessages! [ (102, Handle, Handle<'a>, "ssh_fxp_handle"), (103, Data, Data<'a>, "ssh_fxp_data"), (104, Name, Name, "ssh_fxp_name"), + (105, Attrs, Attrs, "ssh_fxp_attrs"), }, ]; diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index fc94392d..beb17507 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -3,8 +3,8 @@ use super::PartialWriteRequestTracker; use crate::error::SftpError; use crate::handles::OpaqueFileHandle; use crate::proto::{ - self, InitVersionLowest, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, SftpNum, - SftpPacket, StatusCode, + self, InitVersionLowest, LStat, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, + SftpNum, SftpPacket, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; use crate::server::DirReply; @@ -659,6 +659,23 @@ where .await?; }; } + SftpPacket::LStat(req_id, LStat { file_path: path }) => { + match file_server.liststats(path.as_str()?) { + Ok(attrs) => { + debug!("List stats for {} is {:?}", path, attrs); + + output_producer + .send_packet(&SftpPacket::Attrs(req_id, attrs)) + .await?; + } + Err(status) => { + error!("Error listing stats for {}: {:?}", path, status); + output_producer + .send_status(req_id, status, "Could not list attributes") + .await?; + } + } + } _ => { error!("Unsupported request type: {:?}", request); return Err(SftpError::NotSupported); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 5204c20d..2cc047b4 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -120,7 +120,6 @@ where /// to the client since the client will only stop asking for more elements in the /// directory when a read dir request is answer with an reply.send_eof() /// - #[allow(unused_variables)] async fn readdir( &mut self, @@ -139,6 +138,15 @@ where log::error!("SftpServer RealPath operation not defined: dir = {:?}", dir); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } + + /// Provides the stats of the given file path. It does not follow links + fn liststats(&mut self, file_path: &str) -> SftpOpResult { + log::error!( + "SftpServer ListStats operation not defined: file_path = {:?}", + file_path + ); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } } // TODO Define this @@ -167,25 +175,6 @@ pub struct ChanOut<'g, 'a> { _phantom_a: PhantomData<&'a ()>, // a' Why the second lifetime if ChanIO only needs one } -// TODO: complete this once the flow is fully developed -// `/ The usage is simple: -// / -// / 1. SftpHandler will: Initialize the structure -// / 2. The `SftpServer` trait implementation for 'readdir()' will: -// / -// / - Receive the DirReply ref 'reply' -// / -// / a. If there are items to send: -// / -// / - Instantiate a [`DirEntriesCollection`] with the items in the requested folder -// / - call the 'DirEntriesCollection.SendHeader(reply)' -// / - call the 'DirEntriesCollection.send_entries(reply)' -// / -// / b. If there are no items to send: -// / -// / - Call the 'reply.send_eof()' -// TODO Define this - /// Uses for [`DirReply`] to: /// /// - In case of no more items in the directory to be sent, call `reply.send_eof()` @@ -408,28 +397,52 @@ impl DirEntriesCollection { fn get_attrs_or_empty( maybe_metadata: Result, ) -> Attrs { - maybe_metadata.map(Self::get_attrs).unwrap_or_default() + maybe_metadata.map(get_file_attrs).unwrap_or_default() } - fn get_attrs(metadata: Metadata) -> Attrs { - let time_to_u32 = |time_result: std::io::Result| { - time_result - .ok()? - .duration_since(SystemTime::UNIX_EPOCH) - .ok()? - .as_secs() - .try_into() - .ok() - }; - - Attrs { - size: Some(metadata.len()), - uid: Some(metadata.st_uid()), - gid: Some(metadata.st_gid()), - permissions: Some(metadata.permissions().mode()), - atime: time_to_u32(metadata.accessed()), - mtime: time_to_u32(metadata.modified()), - ext_count: None, - } + // fn get_attrs(metadata: Metadata) -> Attrs { + // let time_to_u32 = |time_result: std::io::Result| { + // time_result + // .ok()? + // .duration_since(SystemTime::UNIX_EPOCH) + // .ok()? + // .as_secs() + // .try_into() + // .ok() + // }; + + // Attrs { + // size: Some(metadata.len()), + // uid: Some(metadata.st_uid()), + // gid: Some(metadata.st_gid()), + // permissions: Some(metadata.permissions().mode()), + // atime: time_to_u32(metadata.accessed()), + // mtime: time_to_u32(metadata.modified()), + // ext_count: None, + // } + // } +} + +#[cfg(feature = "std")] +/// [`std`] helper function to get [`Attrs`] from a [`Metadata`]. +pub fn get_file_attrs(metadata: Metadata) -> Attrs { + let time_to_u32 = |time_result: std::io::Result| { + time_result + .ok()? + .duration_since(SystemTime::UNIX_EPOCH) + .ok()? + .as_secs() + .try_into() + .ok() + }; + + Attrs { + size: Some(metadata.len()), + uid: Some(metadata.st_uid()), + gid: Some(metadata.st_gid()), + permissions: Some(metadata.permissions().mode()), + atime: time_to_u32(metadata.accessed()), + mtime: time_to_u32(metadata.modified()), + ext_count: None, } } From c2a83c2f02deb3c24034bfe495efca224e3f7221 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 16:58:16 +1100 Subject: [PATCH 354/393] [skip ci] Fixed misrepresentation of SSH_FXP_OPEN packet also fixing an error with no_std for std helper function Removed doc orphan document comments --- sftp/src/lib.rs | 10 ++-- sftp/src/proto.rs | 143 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 6 deletions(-) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 9050cbb1..4e514b6d 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -60,10 +60,10 @@ mod sftpserver; mod sftpsink; mod sftpsource; -/// Main calling point for the library provided that the user implements -/// a [`crate::sftpserver::SftpServer`]. -/// -/// Please see basic usage at `../demo/sftd/std` +// Main calling point for the library provided that the user implements +// a [`server::SftpServer`]. +// +// Please see basic usage at `../demo/sftd/std` pub use sftphandler::SftpHandler; /// Structures and types used to add the details for the target system @@ -84,6 +84,7 @@ pub mod server { #[cfg(feature = "std")] pub use crate::sftpserver::DirEntriesCollection; + #[cfg(feature = "std")] pub use crate::sftpserver::get_file_attrs; } pub use crate::sftpsink::SftpSink; @@ -104,6 +105,7 @@ pub mod protocol { pub use crate::proto::Filename; pub use crate::proto::Name; pub use crate::proto::NameEntry; + pub use crate::proto::PFlags; pub use crate::proto::PathInfo; pub use crate::proto::StatusCode; /// Constants that might be useful for SFTP developers diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 6fab4e74..8c7b6654 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -106,11 +106,66 @@ pub struct Open<'a> { /// The relative or absolute path of the file to be open pub filename: Filename<'a>, /// File [permissions flags](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3) - pub pflags: u32, + pub pflags: PFlags, /// Initial attributes for the file pub attrs: Attrs, } +/// Flags for Open RequestFor more information see [Opening, creating and closing files](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.3) +/// TODO: Reference! This is packed as u32 since that is the field data type in specs +#[derive(Debug, FromPrimitive, PartialEq)] +#[repr(u32)] +#[allow(non_camel_case_types, missing_docs)] +pub enum PFlags { + //#[sshwire(variant = "ssh_fx_read")] + SSH_FXF_READ = 0x00000001, + //#[sshwire(variant = "ssh_fx_write")] + SSH_FXF_WRITE = 0x00000002, + //#[sshwire(variant = "ssh_fx_append")] + SSH_FXF_APPEND = 0x00000004, + //#[sshwire(variant = "ssh_fx_creat")] + SSH_FXF_CREAT = 0x00000008, + //#[sshwire(variant = "ssh_fx_trunk")] + SSH_FXF_TRUNC = 0x00000010, + //#[sshwire(variant = "ssh_fx_excl")] + SSH_FXF_EXCL = 0x00000020, + //#[sshwire(unknown)] + #[num_enum(catch_all)] + Multiple(u32), +} + +impl<'de> SSHDecode<'de> for PFlags { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + Ok(PFlags::from(u32::dec(s)?)) + } +} + +// TODO: Implement an automatic from implementation for u32 to Status code +// This is prone to errors if we update PFlags enum +impl From<&PFlags> for u32 { + fn from(value: &PFlags) -> Self { + match value { + PFlags::SSH_FXF_READ => 0x00000001, + PFlags::SSH_FXF_WRITE => 0x00000002, + PFlags::SSH_FXF_APPEND => 0x00000004, + PFlags::SSH_FXF_CREAT => 0x00000008, + PFlags::SSH_FXF_TRUNC => 0x00000010, + PFlags::SSH_FXF_EXCL => 0x00000020, + PFlags::Multiple(value) => *value, + } + } +} +// TODO: Implement an SSHEncode attribute for enums to encode them in a given numeric format +impl SSHEncode for PFlags { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + let numeric_value: u32 = self.into(); + numeric_value.enc(s) + } +} + /// Used for `ssh_fxp_open` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.7). #[derive(Debug, SSHEncode, SSHDecode)] pub struct OpenDir<'a> { @@ -166,6 +221,14 @@ pub struct LStat<'a> { pub file_path: TextString<'a>, } +/// Used for `ssh_fxp_lstat` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8). +/// STAT does follow symbolic links +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct Stat<'a> { + /// The path of the element which stats are to be retrieved + pub file_path: TextString<'a>, +} + // ============================= Responses ============================= /// Used for `ssh_fxp_realpath` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.11). @@ -367,7 +430,7 @@ pub struct ExtPair<'a> { /// See [File Attributes](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#autoid-5) /// for more information. #[allow(missing_docs)] -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct Attrs { pub size: Option, pub uid: Option, @@ -860,6 +923,7 @@ sftpmessages! [ (11, OpenDir, OpenDir<'a>, "ssh_fxp_opendir"), (12, ReadDir, ReadDir<'a>, "ssh_fxp_readdir"), (16, PathInfo, PathInfo<'a>, "ssh_fxp_realpath"), + (17, Stat, Stat<'a>, "ssh_fxp_stat"), }, response: { @@ -876,6 +940,14 @@ mod proto_tests { use super::*; use crate::server::SftpSink; + // TODO: Create tests for every SftpPacket. A good starting point is a + // roadtrip test + + #[cfg(test)] + extern crate std; + #[cfg(test)] + use std::println; + #[test] fn test_status_encoding() { let mut buf = [0u8; 256]; @@ -905,4 +977,71 @@ mod proto_tests { assert_eq!(&expected_status_packet_slice, sink.used_slice()); } + + #[test] + fn test_attributes_roundtrip() { + let mut buff = [0u8; MAX_NAME_ENTRY_SIZE]; + let attr_read_only = Attrs { + size: Some(1), + uid: Some(2), + gid: Some(3), + permissions: Some(222), + atime: Some(4), + mtime: Some(5), + ext_count: None, + // ext_count: Some(10), // TODO: This does not get deserialized + }; + + let mut sink = SftpSink::new(&mut buff); + attr_read_only.enc(&mut sink).unwrap(); + println!( + "attr_read_only encoded_len = {:?}, encoded = {:?}", + sink.payload_len(), + sink.payload_slice() + ); + let mut source = SftpSource::new(sink.payload_slice()); + println!("source = {:?}", source); + + let a_r = Attrs::dec(&mut source); + match a_r { + Ok(attrs) => { + println!("source = {:?}", attrs); + assert_eq!(attr_read_only, attrs); + } + Err(e) => panic!("The attributes could not be decoded: {:?}", e), + } + } + + #[test] + fn test_packet_open_reading() { + let buff_open_read = [ + 0u8, 0, 0, + 58, // Len + 3, // SftpPacket + 0, 0, 0, + 4, // ReqId + 0, 0, 0, + 41, // Text String len + 46, 47, 100, 101, 109, 111, 47, 115, 102, + 116, // file Path + 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, + 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, + 109, // and 41 + 0, 0, 0, + 1, // PFlags: 1u32 == SSSH_FXF_READ + 0, 0, 0, + 0, // Attrib flags == 0 No flags, no attributes + ]; + + let mut source = SftpSource::new(&buff_open_read); + println!("source = {:?}", source); + + match SftpPacket::decode_request(&mut source) { + Ok(SftpPacket::Open(req_id, open)) => { + assert_eq!(PFlags::SSH_FXF_READ, open.pflags); + } + Ok(other) => panic!("Expected Open packet, got: {:?}", other), + Err(e) => panic!("Failed to decode packet: {:?}", e), + } + } } From bcf89e1546507c6ac666cecb581fae05efc261c2 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 17 Nov 2025 17:04:17 +1100 Subject: [PATCH 355/393] Fixing SftpTrait and implementations to use the proper Open mode (PFlags) Tested with `test_get.sh` Next step: Implement SSH_FXP_READ --- demo/sftp/std/src/demosftpserver.rs | 52 ++++++++++++++++++----------- demo/sftp/std/testing/test_get.sh | 35 +++++++++++++++++-- sftp/src/sftphandler/sftphandler.rs | 23 +++++++++++-- sftp/src/sftpserver.rs | 36 +++++--------------- 4 files changed, 94 insertions(+), 52 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 72193b82..2fbbfc01 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -4,7 +4,7 @@ use crate::{ }; use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; -use sunset_sftp::protocol::{Attrs, Filename, Name, NameEntry, StatusCode}; +use sunset_sftp::protocol::{Attrs, Filename, NameEntry, PFlags, StatusCode}; use sunset_sftp::server::helpers::DirEntriesCollection; use sunset_sftp::server::{ DirReply, ReadReply, ReadStatus, SftpOpResult, SftpServer, @@ -15,6 +15,8 @@ use log::{debug, error, info, log, trace, warn}; use std::fs; use std::{fs::File, os::unix::fs::FileExt, path::Path}; +use std::os::unix::fs::PermissionsExt; + #[derive(Debug)] pub(crate) enum PrivatePathHandle { File(PrivateFileHandle), @@ -105,16 +107,13 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { fn open( &mut self, filename: &str, - attrs: &Attrs, + mode: &PFlags, ) -> SftpOpResult { - debug!("Open file: filename = {:?}, attributes = {:?}", filename, attrs); - - let poxit_attr = attrs - .permissions - .as_ref() - .ok_or(StatusCode::SSH_FX_PERMISSION_DENIED)?; - let can_write = poxit_attr & 0o222 > 0; - let can_read = poxit_attr & 0o444 > 0; + debug!("Open file: filename = {:?}, mode = {:?}", filename, mode); + + let can_write = u32::from(mode) & u32::from(&PFlags::SSH_FXF_WRITE) > 0; + let can_read = u32::from(mode) & u32::from(&PFlags::SSH_FXF_READ) > 0; + debug!( "File open for read/write access: can_read={:?}, can_write={:?}", can_read, can_write @@ -123,14 +122,21 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { let file = File::options() .read(can_read) .write(can_write) - .create(true) + .create(can_write) .open(filename) .map_err(|_| StatusCode::SSH_FX_FAILURE)?; + let permissions = file + .metadata() + .map_err(|_| StatusCode::SSH_FX_FAILURE)? + .permissions() + .mode() + & 0o777; + let fh = self.handles_manager.insert( PrivatePathHandle::File(PrivateFileHandle { path: filename.into(), - permissions: attrs.permissions, + permissions: Some(permissions), file, }), OPAQUE_SALT, @@ -324,16 +330,24 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } } - fn liststats(&mut self, file_path: &str) -> SftpOpResult { + fn stats(&mut self, follow_links: bool, file_path: &str) -> SftpOpResult { log::debug!("SftpServer ListStats: file_path = {:?}", file_path); let file_path = Path::new(file_path); + + let metadata = if follow_links { + file_path.metadata() // follows symlinks + } else { + file_path.symlink_metadata() // doesn't follow symlinks + } + .map_err(|err| { + error!("Problem listing stats: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; + if file_path.is_file() { - return Ok(sunset_sftp::server::helpers::get_file_attrs( - file_path.metadata().map_err(|err| { - error!("Problem listing stats: {:?}", err); - StatusCode::SSH_FX_FAILURE - })?, - )); + return Ok(sunset_sftp::server::helpers::get_file_attrs(metadata)); + } else if file_path.is_symlink() { + return Ok(sunset_sftp::server::helpers::get_file_attrs(metadata)); } else { return Err(StatusCode::SSH_FX_NO_SUCH_FILE); } diff --git a/demo/sftp/std/testing/test_get.sh b/demo/sftp/std/testing/test_get.sh index e191895f..d4f007b6 100755 --- a/demo/sftp/std/testing/test_get.sh +++ b/demo/sftp/std/testing/test_get.sh @@ -15,13 +15,44 @@ dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." -# Upload all files +# Upload the files sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'put ./%s\n' "${FILES[@]}") -$(printf 'get ./%s\n' "${FILES[@]}") bye EOF +echo "UPLOAD Test Results:" +echo "=============" +# Test each file +for file in "${FILES[@]}"; do + if diff "./${file}" "./out/${file}" >/dev/null 2>&1; then + echo "Upload PASS: ${file}" + else + echo "Upload FAIL: ${file}" + fi +done + +echo "Cleaning up original files..." +rm -f -r ./*_random + +# Download the files +sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +$(printf 'get ./%s\n' "${FILES[@]}") +bye +EOF + +echo "DOWNLOAD Test Results:" +echo "=============" +# Test each file +for file in "${FILES[@]}"; do + if diff "./${file}" "./out/${file}" >/dev/null 2>&1; then + echo "Download PASS: ${file}" + else + echo "Download FAIL: ${file}" + fi +done + + echo "Cleaning up local files..." rm -f -r ./*_random ./out/*_random diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index beb17507..9716b4bd 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -4,7 +4,7 @@ use crate::error::SftpError; use crate::handles::OpaqueFileHandle; use crate::proto::{ self, InitVersionLowest, LStat, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, - SftpNum, SftpPacket, StatusCode, + SftpNum, SftpPacket, Stat, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; use crate::server::DirReply; @@ -563,7 +563,7 @@ where dir_reply.send_item(&name_entry).await?; } SftpPacket::Open(req_id, open) => { - match file_server.open(open.filename.as_str()?, &open.attrs) { + match file_server.open(open.filename.as_str()?, &open.pflags) { Ok(opaque_file_handle) => { let response = SftpPacket::Handle( req_id, @@ -660,7 +660,24 @@ where }; } SftpPacket::LStat(req_id, LStat { file_path: path }) => { - match file_server.liststats(path.as_str()?) { + match file_server.stats(false, path.as_str()?) { + Ok(attrs) => { + debug!("List stats for {} is {:?}", path, attrs); + + output_producer + .send_packet(&SftpPacket::Attrs(req_id, attrs)) + .await?; + } + Err(status) => { + error!("Error listing stats for {}: {:?}", path, status); + output_producer + .send_status(req_id, status, "Could not list attributes") + .await?; + } + } + } + SftpPacket::Stat(req_id, Stat { file_path: path }) => { + match file_server.stats(true, path.as_str()?) { Ok(attrs) => { debug!("List stats for {} is {:?}", path, attrs); diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 2cc047b4..5c954443 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,6 +1,6 @@ use crate::error::SftpResult; use crate::proto::{ - ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, MAX_NAME_ENTRY_SIZE, NameEntry, + ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, MAX_NAME_ENTRY_SIZE, NameEntry, PFlags, }; use crate::server::SftpSink; use crate::sftphandler::SftpOutputProducer; @@ -51,11 +51,11 @@ where T: OpaqueFileHandle, { /// Opens a file for reading/writing - fn open(&'_ mut self, path: &str, attrs: &Attrs) -> SftpOpResult { + fn open(&'_ mut self, path: &str, mode: &PFlags) -> SftpOpResult { log::error!( "SftpServer Open operation not defined: path = {:?}, attrs = {:?}", path, - attrs + mode ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } @@ -139,10 +139,12 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } - /// Provides the stats of the given file path. It does not follow links - fn liststats(&mut self, file_path: &str) -> SftpOpResult { + /// Provides the stats of the given file path + fn stats(&mut self, follow_links: bool, file_path: &str) -> SftpOpResult { log::error!( - "SftpServer ListStats operation not defined: file_path = {:?}", + "SftpServer Stats operation not defined: follow_link = {:?}, \ + file_path = {:?}", + follow_links, file_path ); Err(StatusCode::SSH_FX_OP_UNSUPPORTED) @@ -399,28 +401,6 @@ impl DirEntriesCollection { ) -> Attrs { maybe_metadata.map(get_file_attrs).unwrap_or_default() } - - // fn get_attrs(metadata: Metadata) -> Attrs { - // let time_to_u32 = |time_result: std::io::Result| { - // time_result - // .ok()? - // .duration_since(SystemTime::UNIX_EPOCH) - // .ok()? - // .as_secs() - // .try_into() - // .ok() - // }; - - // Attrs { - // size: Some(metadata.len()), - // uid: Some(metadata.st_uid()), - // gid: Some(metadata.st_gid()), - // permissions: Some(metadata.permissions().mode()), - // atime: time_to_u32(metadata.accessed()), - // mtime: time_to_u32(metadata.modified()), - // ext_count: None, - // } - // } } #[cfg(feature = "std")] From f45cebbf64337827cca8cf6bc591585f7faa87a8 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 18 Nov 2025 14:24:20 +1100 Subject: [PATCH 356/393] WIP: Added SFTP Get capability. Chasing unexpected locks After sending a full SSH_FXA_DATA response, sometimes the process stalls and there is no further request from the client. Looks like this happens when the server answer one response behind the last SSH_FXP_READ request. See client verbose output ``` sftp> get ./65kB_random debug3: Looking up ./demo/sftp/std/testing/out/./65kB_random debug3: Sent message fd 3 T:7 I:21 debug3: Received stat reply T:105 I:21 F:0x000f M:100644 Fetching ./demo/sftp/std/testing/out/./65kB_random to 65kB_random debug2: do_download: download remote "./demo/sftp/std/testing/out/./65kB_random" to local "65kB_random" debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./65kB_random" debug3: Sent message fd 3 T:17 I:22 debug3: Received stat reply T:105 I:22 F:0x000f M:100644 debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./65kB_random" debug3: Sent remote message SSH2_FXP_OPEN I:23 P:./demo/sftp/std/testing/out/./65kB_random M:0x0001 65kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) debug2: channel 0: window 1998336 sent adjust 98816 debug3: Received reply T:103 I:24 R:1 debug3: Received data 0 -> 32767 debug3: Request range 32768 -> 65535 (0/2) debug3: Request range 65536 -> 98303 (1/2) debug3: Received reply T:103 I:25 R:2 debug3: Received data 32768 -> 65535 debug3: Finish at 98304 ( 1) ``` --- demo/sftp/std/src/demosftpserver.rs | 103 +++++++++++++++++++++++++--- demo/sftp/std/src/main.rs | 4 +- demo/sftp/std/testing/test_get.sh | 12 +++- sftp/src/proto.rs | 14 ++++ sftp/src/sftperror.rs | 5 ++ sftp/src/sftphandler/sftphandler.rs | 41 ++++++++++- sftp/src/sftpserver.rs | 91 +++++++++++++++++------- 7 files changed, 229 insertions(+), 41 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 2fbbfc01..8979138a 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -3,6 +3,7 @@ use crate::{ demoopaquefilehandle::DemoOpaqueFileHandle, }; +use sunset_sftp::error::SftpResult; use sunset_sftp::handles::{OpaqueFileHandleManager, PathFinder}; use sunset_sftp::protocol::{Attrs, Filename, NameEntry, PFlags, StatusCode}; use sunset_sftp::server::helpers::DirEntriesCollection; @@ -13,9 +14,8 @@ use sunset_sftp::server::{ #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; use std::fs; -use std::{fs::File, os::unix::fs::FileExt, path::Path}; - use std::os::unix::fs::PermissionsExt; +use std::{fs::File, os::unix::fs::FileExt, path::Path}; #[derive(Debug)] pub(crate) enum PrivatePathHandle { @@ -220,18 +220,99 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } } - fn read( + async fn read( &mut self, opaque_file_handle: &DemoOpaqueFileHandle, offset: u64, - _reply: &mut ReadReply<'_, '_>, - ) -> SftpOpResult<()> { - log::error!( - "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", - opaque_file_handle, - offset - ); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + len: u32, + reply: &ReadReply<'_, N>, + ) -> SftpResult<()> { + if let PrivatePathHandle::File(private_file_handle) = self + .handles_manager + .get_private_as_mut_ref(opaque_file_handle) + .ok_or(StatusCode::SSH_FX_FAILURE)? + { + log::debug!( + "SftpServer Read operation: handle = {:?}, filepath = {:?}, offset = {:?}, len = {:?}", + opaque_file_handle, + private_file_handle.path, + offset, + len + ); + let permissions_poxit = private_file_handle.permissions.unwrap_or(0o000); + if (permissions_poxit & 0o444) == 0 { + error!( + "No read permissions for file {:?}", + private_file_handle.path + ); + return Err(StatusCode::SSH_FX_PERMISSION_DENIED.into()); + }; + + let file_len = private_file_handle + .file + .metadata() + .map_err(|err| { + error!("Could not read the file length: {:?}", err); + StatusCode::SSH_FX_FAILURE + })? + .len(); + + if offset >= file_len { + info!("offset is larger than file length, sending EOF"); + reply.send_eof().await.map_err(|err| { + error!("Could not sent EOF: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; + return Ok(()); + } + + let read_len = if file_len >= len as u64 + offset { + len + } else { + warn!("Read operation: length + offset > file length. Clipping ( {:?} + {:?} > {:?})", + len, offset, file_len); + (file_len - offset).try_into().unwrap_or(u32::MAX) + }; + + reply.send_header(offset, read_len).await?; + + const ARBITRARY_BUFFER_LENGTH: usize = 1024; + + let mut read_buff = [0u8; ARBITRARY_BUFFER_LENGTH]; + + let mut running_offset = offset; + let mut remaining = read_len as usize; + + debug!("Starting reading loop: remaining = {}", remaining); + while remaining > 0 { + let next_read_len: usize = remaining.min(read_buff.len()); + debug!("next_read_len = {}", next_read_len); + let br = private_file_handle + .file + .read_at(&mut read_buff[..next_read_len], running_offset) + .map_err(|err| { + error!("read error: {:?}", err); + StatusCode::SSH_FX_FAILURE + })?; + debug!("{} bytes readed", br); + reply.send_data(&read_buff[..br.min(remaining)]).await?; + debug!("Read sent {} bytes", br.min(remaining)); + debug!("remaining {} bytes. {} byte read", remaining, br); + + remaining = + remaining.checked_sub(br).ok_or(StatusCode::SSH_FX_FAILURE)?; + debug!( + "after substracting {} bytes, there are {} bytes remaining", + br, remaining + ); + running_offset = running_offset + .checked_add(br as u64) + .ok_or(StatusCode::SSH_FX_FAILURE)?; + } + debug!("Finished sending data"); + return Ok(()); + } + Err(StatusCode::SSH_FX_PERMISSION_DENIED.into()) } fn write( diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 7ddf09a8..7218d6ce 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -205,7 +205,7 @@ async fn listen( #[embassy_executor::main] async fn main(spawner: Spawner) { env_logger::builder() - .filter_level(log::LevelFilter::Debug) + .filter_level(log::LevelFilter::Info) .filter_module( "sunset_demo_sftp_std::demosftpserver", log::LevelFilter::Debug, @@ -213,7 +213,7 @@ async fn main(spawner: Spawner) { .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Debug) .filter_module( "sunset_sftp::sftphandler::sftpoutputchannelhandler", - log::LevelFilter::Trace, + log::LevelFilter::Info, ) // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) diff --git a/demo/sftp/std/testing/test_get.sh b/demo/sftp/std/testing/test_get.sh index d4f007b6..e498fee9 100755 --- a/demo/sftp/std/testing/test_get.sh +++ b/demo/sftp/std/testing/test_get.sh @@ -7,16 +7,21 @@ REMOTE_HOST="192.168.69.2" REMOTE_USER="any" # Define test files -FILES=("512B_random") +FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random") # Generate random data files echo "Generating random data files..." dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null - +dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=65 of=./65kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=256 of=./256kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=1024 of=./1024kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=2048 of=./2048kB_random 2>/dev/null echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." # Upload the files -sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'put ./%s\n' "${FILES[@]}") bye @@ -39,6 +44,7 @@ rm -f -r ./*_random # Download the files sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'get ./%s\n' "${FILES[@]}") + bye EOF diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 8c7b6654..8dc52b30 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -268,6 +268,20 @@ pub struct Data<'a> { pub data: BinString<'a>, } +/// This is the encoded length for the [`Data`] Sftp Response. +/// +/// This considers the Packet type (1), the request ID (4), and the data string +/// length (4) +/// +/// - It excludes explicitly length field for the SftpPacket +/// - It excludes explicitly length of the data string content +/// +/// It is defined a single source of truth for what is the length for the +/// encoded [`SftpPacket::Data`] variant +/// +/// See [Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) +pub(crate) const ENCODED_BASE_DATA_SFTP_PACKET_LENGTH: u32 = 1 + 4 + 4; + /// Struct to hold `SSH_FXP_NAME` response. /// See [SSH_FXP_NAME in Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) #[derive(Debug, SSHEncode, SSHDecode)] diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index cd01f748..8fd251d9 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -61,6 +61,11 @@ impl From for SftpError { SftpError::RequestHolderError(value) } } +// impl From for SftpError { +// fn from(value: FileServerError) -> Self { +// SftpError::FileServerError(value) +// } +// } impl From for WireError { fn from(value: SftpError) -> Self { diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 9716b4bd..bbf2c3ec 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -7,7 +7,7 @@ use crate::proto::{ SftpNum, SftpPacket, Stat, StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; -use crate::server::DirReply; +use crate::server::{DirReply, ReadReply}; use crate::sftperror::SftpResult; use crate::sftphandler::sftpoutputchannelhandler::{ SftpOutputPipe, SftpOutputProducer, @@ -659,6 +659,45 @@ where .await?; }; } + SftpPacket::Read(req_id, read) => { + match file_server + .read( + &T::try_from(&read.handle)?, + read.offset, + read.len, + &ReadReply::new(req_id, output_producer), + ) + .await + { + Ok(()) => { + // debug!("List stats for {} is {:?}", path, attrs); + + // output_producer + // .send_packet(&SftpPacket::Attrs(req_id, attrs)) + // .await?; + } + Err(error) => { + error!("Error reading data: {:?}", error); + if let SftpError::FileServerError(status) = error { + output_producer + .send_status( + req_id, + status, + "Could not list attributes", + ) + .await?; + } else { + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_FAILURE, + "Could not list attributes", + ) + .await?; + } + } + } + } SftpPacket::LStat(req_id, LStat { file_path: path }) => { match file_server.stats(false, path.as_str()?) { Ok(attrs) => { diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 5c954443..ab58ce75 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,6 +1,7 @@ -use crate::error::SftpResult; +use crate::error::{SftpError, SftpResult}; use crate::proto::{ - ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, MAX_NAME_ENTRY_SIZE, NameEntry, PFlags, + ENCODED_BASE_DATA_SFTP_PACKET_LENGTH, ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, + MAX_NAME_ENTRY_SIZE, NameEntry, PFlags, }; use crate::server::SftpSink; use crate::sftphandler::SftpOutputProducer; @@ -11,7 +12,6 @@ use crate::{ use sunset::sshwire::SSHEncode; -use core::marker::PhantomData; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; @@ -67,18 +67,21 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } /// Reads from a file that has previously being opened for reading - fn read( + #[allow(unused)] + async fn read( &mut self, opaque_file_handle: &T, offset: u64, - _reply: &mut ReadReply<'_, '_>, - ) -> SftpOpResult<()> { + len: u32, + reply: &ReadReply<'_, N>, + ) -> SftpResult<()> { log::error!( - "SftpServer Read operation not defined: handle = {:?}, offset = {:?}", + "SftpServer Read operation not defined: handle = {:?}, offset = {:?}, len = {:?}", opaque_file_handle, - offset + offset, + len ); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + Err(SftpError::FileServerError(StatusCode::SSH_FX_OP_UNSUPPORTED)) } /// Writes to a file that has previously being opened for writing fn write( @@ -156,25 +159,65 @@ where /// A reference structure passed to the [`SftpServer::read()`] method to /// allow replying with the read data. -pub struct ReadReply<'g, 'a> { - chan: ChanOut<'g, 'a>, +pub struct ReadReply<'g, const N: usize> { + /// The request Id that will be use`d in the response + req_id: ReqId, + + /// Immutable writer + chan_out: &'g SftpOutputProducer<'g, N>, } -impl<'g, 'a> ReadReply<'g, 'a> { - /// **This is a work in progress** - /// - /// Reply with a slice containing the read data - /// It can be called several times to send multiple data chunks +impl<'g, const N: usize> ReadReply<'g, N> { + /// New instances can only be created within the crate. Users can only + /// use other public methods to use it. + pub(crate) fn new( + req_id: ReqId, + chan_out: &'g SftpOutputProducer<'g, N>, + ) -> Self { + ReadReply { req_id, chan_out } + } + + // TODO Make this enforceable + // TODO Document + pub async fn send_header(&self, offset: u64, data_len: u32) -> SftpResult<()> { + debug!( + "ReadReply: Sending header for request id {:?}: offset = {:?}, data length = {:?}", + self.req_id, offset, data_len + ); + let mut s = [0u8; N]; + let mut sink = SftpSink::new(&mut s); + + // Encoding length field + (data_len + ENCODED_BASE_DATA_SFTP_PACKET_LENGTH).enc(&mut sink)?; + // Encoding packet type + 103u8.enc(&mut sink)?; // TODO Replace hack with + // Encoding req_id + self.req_id.enc(&mut sink)?; + // string data length + data_len.enc(&mut sink)?; + + let payload = sink.payload_slice(); + debug!( + "Sending header: len = {:?}, content = {:?}", + payload.len(), + payload + ); + // Sending payload_slice since we are not making use of the sink sftpPacket length calculation + self.chan_out.send_data(payload).await?; + Ok(()) + } + + /// Sends a buffer with data /// - /// **Important**: The first reply should contain the header - #[allow(unused_variables)] - pub fn reply(self, data: &[u8]) {} -} + /// Call this + pub async fn send_data(&self, buff: &[u8]) -> SftpResult<()> { + self.chan_out.send_data(buff).await + } -// TODO Implement correct Channel Out -pub struct ChanOut<'g, 'a> { - _phantom_g: PhantomData<&'g ()>, // 'g look what these might be ChanIO lifetime - _phantom_a: PhantomData<&'a ()>, // a' Why the second lifetime if ChanIO only needs one + /// Sends EOF meaning that there is no more files in the directory + pub async fn send_eof(&self) -> SftpResult<()> { + self.chan_out.send_status(self.req_id, StatusCode::SSH_FX_EOF, "").await + } } /// Uses for [`DirReply`] to: From 60cf7e5bc5484d0787f09d6969d7bbbe498ecbff Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 18 Nov 2025 15:18:48 +1100 Subject: [PATCH 357/393] SFTP Read stuck: Failing to receive a a read request? Anyone can take a look at this? Following the analysis in the previous commit, it happens that from time to time there is no more data to be read from the SSH Chan_In while the client is waiting for a response. I have documented this with some logs extracts See files failing-get-client.log, failing-get-server.log (sucess-get{client, server}.log provided for reference). In those failing examples, the server looks stuck waiting to receive new bytes from the channel (SFTP: About to read bytes from SSH Channel) During another run, while in the deadlock, I have manually killed the sftp client (`pkill sftp`) and the server logs receives an rx complete and listen again for new connections (get-deadlock-client-killed.log) --- demo/sftp/std/src/demosftpserver.rs | 13 +- demo/sftp/std/src/main.rs | 2 +- .../std/testing/out/failing-get-client.log | 294 +++++++++++ .../std/testing/out/failing-get-server.log | 471 ++++++++++++++++++ .../out/get-deadlock-client-killed.log | 55 ++ .../std/testing/out/success-get-client.log | 308 ++++++++++++ .../std/testing/out/success-get-server.log | 351 +++++++++++++ demo/sftp/std/testing/test_get.sh | 4 +- sftp/src/sftphandler/sftphandler.rs | 1 + 9 files changed, 1490 insertions(+), 9 deletions(-) create mode 100644 demo/sftp/std/testing/out/failing-get-client.log create mode 100644 demo/sftp/std/testing/out/failing-get-server.log create mode 100644 demo/sftp/std/testing/out/get-deadlock-client-killed.log create mode 100644 demo/sftp/std/testing/out/success-get-client.log create mode 100644 demo/sftp/std/testing/out/success-get-server.log diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 8979138a..a78f8055 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -286,7 +286,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { debug!("Starting reading loop: remaining = {}", remaining); while remaining > 0 { let next_read_len: usize = remaining.min(read_buff.len()); - debug!("next_read_len = {}", next_read_len); + trace!("next_read_len = {}", next_read_len); let br = private_file_handle .file .read_at(&mut read_buff[..next_read_len], running_offset) @@ -294,16 +294,17 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { error!("read error: {:?}", err); StatusCode::SSH_FX_FAILURE })?; - debug!("{} bytes readed", br); + trace!("{} bytes readed", br); reply.send_data(&read_buff[..br.min(remaining)]).await?; - debug!("Read sent {} bytes", br.min(remaining)); - debug!("remaining {} bytes. {} byte read", remaining, br); + trace!("Read sent {} bytes", br.min(remaining)); + trace!("remaining {} bytes. {} byte read", remaining, br); remaining = remaining.checked_sub(br).ok_or(StatusCode::SSH_FX_FAILURE)?; - debug!( + trace!( "after substracting {} bytes, there are {} bytes remaining", - br, remaining + br, + remaining ); running_offset = running_offset .checked_add(br as u64) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 7218d6ce..4f65f975 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -210,7 +210,7 @@ async fn main(spawner: Spawner) { "sunset_demo_sftp_std::demosftpserver", log::LevelFilter::Debug, ) - .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Debug) + .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) .filter_module( "sunset_sftp::sftphandler::sftpoutputchannelhandler", log::LevelFilter::Info, diff --git a/demo/sftp/std/testing/out/failing-get-client.log b/demo/sftp/std/testing/out/failing-get-client.log new file mode 100644 index 00000000..3585756c --- /dev/null +++ b/demo/sftp/std/testing/out/failing-get-client.log @@ -0,0 +1,294 @@ +Testing Multiple GETs... +Generating random data files... +Uploading files to any@192.168.69.2... +Connected to 192.168.69.2. +sftp> put ./512B_random +Uploading ./512B_random to ./demo/sftp/std/testing/out/512B_random +512B_random 100% 512 443.9KB/s 00:00 +sftp> put ./16kB_random +Uploading ./16kB_random to ./demo/sftp/std/testing/out/16kB_random +16kB_random 100% 16KB 260.0KB/s 00:00 +sftp> put ./64kB_random +Uploading ./64kB_random to ./demo/sftp/std/testing/out/64kB_random +64kB_random 100% 64KB 255.3KB/s 00:00 +sftp> put ./65kB_random +Uploading ./65kB_random to ./demo/sftp/std/testing/out/65kB_random +65kB_random 100% 65KB 201.3KB/s 00:00 +sftp> +sftp> bye +client_loop: send disconnect: Broken pipe +UPLOAD Test Results: +============= +Upload PASS: 512B_random +Upload PASS: 16kB_random +Upload PASS: 64kB_random +Upload PASS: 65kB_random +Cleaning up original files... +OpenSSH_8.9p1 Ubuntu-3ubuntu0.13, OpenSSL 3.0.2 15 Mar 2022 +debug1: Reading configuration data /home/jubeor/.ssh/config +debug1: Reading configuration data /etc/ssh/ssh_config +debug1: /etc/ssh/ssh_config line 19: include /etc/ssh/ssh_config.d/*.conf matched no files +debug1: /etc/ssh/ssh_config line 21: Applying options for * +debug2: resolve_canonicalize: hostname 192.168.69.2 is address +debug3: ssh_connect_direct: entering +debug1: Connecting to 192.168.69.2 [192.168.69.2] port 22. +debug3: set_sock_tos: set socket 3 IP_TOS 0x10 +debug1: Connection established. +debug1: identity file /home/jubeor/.ssh/id_rsa type 0 +debug1: identity file /home/jubeor/.ssh/id_rsa-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa_sk type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa_sk-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519 type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519_sk type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519_sk-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_xmss type -1 +debug1: identity file /home/jubeor/.ssh/id_xmss-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_dsa type -1 +debug1: identity file /home/jubeor/.ssh/id_dsa-cert type -1 +debug1: Local version string SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.13 +debug1: Remote protocol version 2.0, remote software version Sunset-1 +debug1: compat_banner: no match: Sunset-1 +debug2: fd 3 setting O_NONBLOCK +debug1: Authenticating to 192.168.69.2:22 as 'any' +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory +debug3: order_hostkeyalgs: no algorithms matched; accept original +debug3: send packet: type 20 +debug1: SSH2_MSG_KEXINIT sent +debug3: receive packet: type 20 +debug1: SSH2_MSG_KEXINIT received +debug2: local client KEXINIT proposal +debug2: KEX algorithms: curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,sntrup761x25519-sha512@openssh.com,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,ext-info-c,kex-strict-c-v00@openssh.com +debug2: host key algorithms: ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,sk-ecdsa-sha2-nistp256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256 +debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com +debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com +debug2: MACs ctos: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 +debug2: MACs stoc: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 +debug2: compression ctos: none,zlib@openssh.com,zlib +debug2: compression stoc: none,zlib@openssh.com,zlib +debug2: languages ctos: +debug2: languages stoc: +debug2: first_kex_follows 0 +debug2: reserved 0 +debug2: peer server KEXINIT proposal +debug2: KEX algorithms: mlkem768x25519-sha256,curve25519-sha256,curve25519-sha256@libssh.org,kex-strict-s-v00@openssh.com,kexguess2@matt.ucc.asn.au +debug2: host key algorithms: ssh-ed25519,rsa-sha2-256 +debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes256-ctr +debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes256-ctr +debug2: MACs ctos: hmac-sha2-256 +debug2: MACs stoc: hmac-sha2-256 +debug2: compression ctos: none +debug2: compression stoc: none +debug2: languages ctos: +debug2: languages stoc: +debug2: first_kex_follows 0 +debug2: reserved 0 +debug3: kex_choose_conf: will use strict KEX ordering +debug1: kex: algorithm: curve25519-sha256 +debug1: kex: host key algorithm: ssh-ed25519 +debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: compression: none +debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: compression: none +debug3: send packet: type 30 +debug1: expecting SSH2_MSG_KEX_ECDH_REPLY +debug3: receive packet: type 31 +debug1: SSH2_MSG_KEX_ECDH_REPLY received +debug1: Server host key: ssh-ed25519 SHA256:6zdZ/bJAJoPDsk3YaJeckvI8Vbl8JmpTVfJH1ldlhyQ +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory +Warning: Permanently added '192.168.69.2' (ED25519) to the list of known hosts. +debug3: send packet: type 21 +debug1: ssh_packet_send2_wrapped: resetting send seqnr 3 +debug2: ssh_set_newkeys: mode 1 +debug1: rekey out after 134217728 blocks +debug1: SSH2_MSG_NEWKEYS sent +debug1: expecting SSH2_MSG_NEWKEYS +debug3: receive packet: type 21 +debug1: ssh_packet_read_poll2: resetting read seqnr 3 +debug1: SSH2_MSG_NEWKEYS received +debug2: ssh_set_newkeys: mode 0 +debug1: rekey in after 134217728 blocks +debug1: Will attempt key: /home/jubeor/.ssh/id_rsa RSA SHA256:Wc43s4t4VZ5DkRa7At3RMd9E0u/UH4CV75mWxI0G8A4 +debug1: Will attempt key: /home/jubeor/.ssh/id_ecdsa +debug1: Will attempt key: /home/jubeor/.ssh/id_ecdsa_sk +debug1: Will attempt key: /home/jubeor/.ssh/id_ed25519 +debug1: Will attempt key: /home/jubeor/.ssh/id_ed25519_sk +debug1: Will attempt key: /home/jubeor/.ssh/id_xmss +debug1: Will attempt key: /home/jubeor/.ssh/id_dsa +debug2: pubkey_prepare: done +debug3: send packet: type 5 +debug3: receive packet: type 7 +debug1: SSH2_MSG_EXT_INFO received +debug1: kex_input_ext_info: server-sig-algs= +debug3: receive packet: type 6 +debug2: service_accept: ssh-userauth +debug1: SSH2_MSG_SERVICE_ACCEPT received +debug3: send packet: type 50 +debug3: receive packet: type 52 +Authenticated to 192.168.69.2 ([192.168.69.2]:22) using "none". +debug1: channel 0: new [client-session] +debug3: ssh_session2_open: channel_new: 0 +debug2: channel 0: send open +debug3: send packet: type 90 +debug1: Entering interactive session. +debug1: pledge: network +debug3: receive packet: type 91 +debug2: channel_input_open_confirmation: channel 0: callback start +debug2: fd 3 setting TCP_NODELAY +debug3: set_sock_tos: set socket 3 IP_TOS 0x08 +debug2: client_session2_setup: id 0 +debug1: Sending environment. +debug3: Ignored env SHELL +debug3: Ignored env USER_ZDOTDIR +debug3: Ignored env COLORTERM +debug3: Ignored env WSL2_GUI_APPS_ENABLED +debug3: Ignored env TERM_PROGRAM_VERSION +debug3: Ignored env WSL_DISTRO_NAME +debug3: Ignored env NAME +debug3: Ignored env PWD +debug3: Ignored env NIX_PROFILES +debug3: Ignored env LOGNAME +debug3: Ignored env VSCODE_GIT_ASKPASS_NODE +debug3: Ignored env VSCODE_INJECTION +debug3: Ignored env HOME +debug1: channel 0: setting env LANG = "C.UTF-8" +debug2: channel 0: request env confirm 0 +debug3: send packet: type 98 +debug3: Ignored env WSL_INTEROP +debug3: Ignored env LS_COLORS +debug3: Ignored env WAYLAND_DISPLAY +debug3: Ignored env NIX_SSL_CERT_FILE +debug3: Ignored env GIT_ASKPASS +debug3: Ignored env VSCODE_GIT_ASKPASS_EXTRA_ARGS +debug3: Ignored env VSCODE_PYTHON_AUTOACTIVATE_GUARD +debug3: Ignored env TERM +debug3: Ignored env ZDOTDIR +debug3: Ignored env USER +debug3: Ignored env VSCODE_GIT_IPC_HANDLE +debug3: Ignored env DISPLAY +debug3: Ignored env SHLVL +debug3: Ignored env XDG_RUNTIME_DIR +debug3: Ignored env WSLENV +debug3: Ignored env VSCODE_GIT_ASKPASS_MAIN +debug3: Ignored env XDG_DATA_DIRS +debug3: Ignored env PATH +debug3: Ignored env DBUS_SESSION_BUS_ADDRESS +debug3: Ignored env HOSTTYPE +debug3: Ignored env PULSE_SERVER +debug3: Ignored env OLDPWD +debug3: Ignored env TERM_PROGRAM +debug3: Ignored env VSCODE_IPC_HOOK_CLI +debug3: Ignored env _ +debug1: Sending subsystem: sftp +debug2: channel 0: request subsystem confirm 1 +debug3: send packet: type 98 +debug2: channel_input_open_confirmation: channel 0: callback done +debug2: channel 0: open confirm rwindow 1000 rmax 1000 +debug3: receive packet: type 99 +debug2: channel_input_status_confirm: type 99 id 0 +debug2: subsystem request accepted on channel 0 +debug2: Remote version: 3 +Connected to 192.168.69.2. +debug2: Sending SSH2_FXP_REALPATH "." +debug3: Sent message fd 3 T:16 I:1 +debug3: SSH2_FXP_REALPATH . -> ./demo/sftp/std/testing/out/ +sftp> get ./512B_random +debug3: Looking up ./demo/sftp/std/testing/out/./512B_random +debug3: Sent message fd 3 T:7 I:2 +debug3: Received stat reply T:105 I:2 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./512B_random to 512B_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./512B_random" to local "512B_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./512B_random" +debug3: Sent message fd 3 T:17 I:3 +debug3: Received stat reply T:105 I:3 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./512B_random" +debug3: Sent remote message SSH2_FXP_OPEN I:4 P:./demo/sftp/std/testing/out/./512B_random M:0x0001 +512B_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug3: Received reply T:103 I:5 R:1 +debug3: Received data 0 -> 511 +debug3: Short data block, re-requesting 512 -> 32767 ( 1) +debug3: Finish at 32768 ( 1) +debug3: Received reply T:101 I:6 R:1 +512B_random 100% 512 11.0KB/s 00:00 +debug3: Sent message SSH2_FXP_CLOSE I:7 +debug3: SSH2_FXP_STATUS 0 +sftp> get ./16kB_random +debug3: Looking up ./demo/sftp/std/testing/out/./16kB_random +debug3: Sent message fd 3 T:7 I:8 +debug3: Received stat reply T:105 I:8 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./16kB_random to 16kB_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./16kB_random" to local "16kB_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./16kB_random" +debug3: Sent message fd 3 T:17 I:9 +debug2: channel 0: rcvd adjust 516 +debug3: Received stat reply T:105 I:9 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./16kB_random" +debug3: Sent remote message SSH2_FXP_OPEN I:10 P:./demo/sftp/std/testing/out/./16kB_random M:0x0001 +16kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug3: Received reply T:103 I:11 R:1 +debug3: Received data 0 -> 16383 +debug3: Short data block, re-requesting 16384 -> 32767 ( 1) +debug3: Finish at 32768 ( 1) +debug3: Received reply T:101 I:12 R:1 +16kB_random 100% 16KB 332.6KB/s 00:00 +debug3: Sent message SSH2_FXP_CLOSE I:13 +debug3: SSH2_FXP_STATUS 0 +sftp> get ./64kB_random +debug3: Looking up ./demo/sftp/std/testing/out/./64kB_random +debug3: Sent message fd 3 T:7 I:14 +debug3: Received stat reply T:105 I:14 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./64kB_random to 64kB_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./64kB_random" to local "64kB_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./64kB_random" +debug3: Sent message fd 3 T:17 I:15 +debug3: Received stat reply T:105 I:15 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./64kB_random" +debug3: Sent remote message SSH2_FXP_OPEN I:16 P:./demo/sftp/std/testing/out/./64kB_random M:0x0001 +64kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug3: Received reply T:103 I:17 R:1 +debug3: Received data 0 -> 32767 +debug3: Request range 32768 -> 65535 (0/2) +debug3: Request range 65536 -> 98303 (1/2) +debug2: channel 0: rcvd adjust 520 +debug3: Received reply T:103 I:18 R:2 +debug3: Received data 32768 -> 65535 +debug3: Finish at 98304 ( 1) +debug3: Received reply T:101 I:19 R:1 +64kB_random 100% 64KB 631.8KB/s 00:00 +debug3: Sent message SSH2_FXP_CLOSE I:20 +debug3: SSH2_FXP_STATUS 0 +sftp> get ./65kB_random +debug3: Looking up ./demo/sftp/std/testing/out/./65kB_random +debug3: Sent message fd 3 T:7 I:21 +debug3: Received stat reply T:105 I:21 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./65kB_random to 65kB_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./65kB_random" to local "65kB_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./65kB_random" +debug3: Sent message fd 3 T:17 I:22 +debug3: Received stat reply T:105 I:22 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./65kB_random" +debug3: Sent remote message SSH2_FXP_OPEN I:23 P:./demo/sftp/std/testing/out/./65kB_random M:0x0001 +65kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug2: channel 0: window 1998336 sent adjust 98816 +debug3: Received reply T:103 I:24 R:1 +debug3: Received data 0 -> 32767 +debug3: Request range 32768 -> 65535 (0/2) +debug3: Request range 65536 -> 98303 (1/2) +debug3: Received reply T:103 I:25 R:2 +debug3: Received data 32768 -> 65535 +debug3: Finish at 98304 ( 1) +65kB_random 98% 64KB 20.1KB/s - stalled -debug3: send packet: type 1 +debug1: channel 0: free: client-session, nchannels 1 +debug3: channel 0: status: The following connections are open: + #0 client-session (t4 r0 i0/0 o0/0 e[write]/0 fd 4/5/6 sock -1 cc -1 io 0x01/0x02) + +Killed by signal 15. +DOWNLOAD Test Results: +============= +Download PASS: 512B_random +Download PASS: 16kB_random +Download PASS: 64kB_random +Download FAIL: 65kB_random +Cleaning up local files... \ No newline at end of file diff --git a/demo/sftp/std/testing/out/failing-get-server.log b/demo/sftp/std/testing/out/failing-get-server.log new file mode 100644 index 00000000..efcac68d --- /dev/null +++ b/demo/sftp/std/testing/out/failing-get-server.log @@ -0,0 +1,471 @@ + 242, 216, 97, 231, 206, 41, 26, 80, 23, 166, 87, 216, 71, 123, 89, 97, 13, 199, 48, 33, 113, 221, 215, 37, 191, 200, 105, 224, 126, 243, 195, 183, 249, 7, 181, 145, 177, 255, 191, 135, 63, 24, 79, 236, 154, 162, 111, 118, 173, 93, 105, 233, 165, 81, 252, 183, 178, 8, 89, 191, 208, 158, 58, 150, 148, 175, 123, 117, 76, 252, 132, 135, 210, 65, 225, 245, 205, 211, 119, 7, 162, 181, 9, 224, 16, 97, 113, 255, 163, 84, 159, 139, 152, 113, 156, 196, 208, 178, 102, 173, 138, 4, 174, 6, 74, 210, 227, 38, 158, 122, 70, 62, 207, 185, 90, 30, 94, 48, 244, 145, 219, 154, 137, 163, 146, 154, 81, 17, 240, 14, 133, 160, 92, 248, 220, 243, 200, 97, 171, 56, 207, 137, 200, 51, 113, 22, 128, 241, 124, 167, 164, 18, 66, 253, 20, 120, 108, 25, 111, 86, 98, 98, 154, 185, 90, 211, 14, 129, 62, 167, 64, 58, 232, 195, 55, 137, 253, 147, 95, 45, 119, 228, 8, 220, 124, 7, 99, 56, 197, 234, 125, 126, 38, 4, 127, 208, 155, 203, 67, 197, 164, 116, 128, 106, 184, 41, 196, 192, 68, 13, 60, 13, 230, 212, 41, 210, 184, 19, 41, 88, 67, 141, 173, 227, 3, 34, 235, 144, 121, 169, 154, 4, 242, 223, 135, 158, 9, 100, 131, 238, 86, 166, 145, 180, 228, 134, 186, 136, 120, 121, 2, 229, 236, 171, 22, 1, 87, 205, 37, 121, 230, 77, 235, 100, 164, 205, 161, 12, 132, 179, 0, 246, 110, 117, 253, 104, 126, 163, 120, 161, 59, 192, 76, 157, 5, 49, 211, 147, 53], index: 0 } +[2025-11-18T04:07:30.367537929Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 63261, data_segment = BinString(len=512), data remaining = 1763 +[2025-11-18T04:07:30.367572804Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 63261, buffer length = 512, bytes written = 512 +[2025-11-18T04:07:30.367601266Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.367626578Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.367653249Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.367687599Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 296 bytes +[2025-11-18T04:07:30.367695505Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [119, 84, 132, 32, 133, 174, 0, 64, 218, 17, 73, 172, 110, 238, 156, 70, 127, 134, 71, 123, 38, 185, 224, 56, 114, 211, 128, 228, 225, 148, 170, 76, 132, 210, 119, 22, 27, 53, 253, 86, 156, 155, 85, 159, 225, 43, 147, 119, 153, 139, 40, 47, 81, 120, 2, 215, 220, 83, 255, 172, 85, 142, 3, 246, 159, 201, 214, 31, 73, 235, 27, 61, 194, 140, 67, 172, 63, 181, 2, 77, 73, 80, 223, 36, 211, 56, 118, 230, 42, 240, 247, 31, 246, 180, 85, 18, 139, 43, 212, 249, 186, 165, 123, 231, 63, 85, 187, 254, 117, 192, 165, 168, 50, 3, 237, 9, 71, 21, 152, 251, 27, 37, 123, 12, 252, 22, 58, 234, 165, 61, 196, 235, 84, 142, 243, 153, 116, 27, 141, 101, 66, 49, 107, 224, 222, 209, 225, 221, 34, 67, 114, 32, 216, 191, 120, 72, 61, 154, 61, 209, 194, 154, 230, 167, 33, 27, 233, 223, 155, 137, 137, 147, 246, 138, 1, 168, 103, 52, 84, 156, 65, 158, 146, 103, 43, 212, 248, 225, 111, 109, 4, 189, 0, 197, 1, 59, 160, 31, 220, 171, 113, 169, 68, 137, 55, 132, 249, 136, 198, 214, 151, 135, 212, 51, 142, 160, 46, 149, 105, 36, 194, 159, 110, 30, 42, 175, 29, 215, 148, 218, 252, 53, 182, 143, 11, 119, 159, 133, 124, 117, 141, 104, 63, 94, 156, 244, 152, 190, 137, 146, 67, 41, 50, 84, 161, 139, 230, 247, 234, 223, 223, 80, 172, 62, 163, 53, 81, 162, 190, 106, 89, 34, 238, 187, 133, 4, 131, 195, 163, 221, 27, 227, 231, 189, 8, 153, 142, 137, 118, 0, 144, 124, 101, 104, 14, 119] +[2025-11-18T04:07:30.367738605Z TRACE sunset_sftp::sftphandler::sftphandler] Received 296 bytes to process +[2025-11-18T04:07:30.367745676Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.367749403Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 296 +[2025-11-18T04:07:30.367773335Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [119, 84, 132, 32, 133, 174, 0, 64, 218, 17, 73, 172, 110, 238, 156, 70, 127, 134, 71, 123, 38, 185, 224, 56, 114, 211, 128, 228, 225, 148, 170, 76, 132, 210, 119, 22, 27, 53, 253, 86, 156, 155, 85, 159, 225, 43, 147, 119, 153, 139, 40, 47, 81, 120, 2, 215, 220, 83, 255, 172, 85, 142, 3, 246, 159, 201, 214, 31, 73, 235, 27, 61, 194, 140, 67, 172, 63, 181, 2, 77, 73, 80, 223, 36, 211, 56, 118, 230, 42, 240, 247, 31, 246, 180, 85, 18, 139, 43, 212, 249, 186, 165, 123, 231, 63, 85, 187, 254, 117, 192, 165, 168, 50, 3, 237, 9, 71, 21, 152, 251, 27, 37, 123, 12, 252, 22, 58, 234, 165, 61, 196, 235, 84, 142, 243, 153, 116, 27, 141, 101, 66, 49, 107, 224, 222, 209, 225, 221, 34, 67, 114, 32, 216, 191, 120, 72, 61, 154, 61, 209, 194, 154, 230, 167, 33, 27, 233, 223, 155, 137, 137, 147, 246, 138, 1, 168, 103, 52, 84, 156, 65, 158, 146, 103, 43, 212, 248, 225, 111, 109, 4, 189, 0, 197, 1, 59, 160, 31, 220, 171, 113, 169, 68, 137, 55, 132, 249, 136, 198, 214, 151, 135, 212, 51, 142, 160, 46, 149, 105, 36, 194, 159, 110, 30, 42, 175, 29, 215, 148, 218, 252, 53, 182, 143, 11, 119, 159, 133, 124, 117, 141, 104, 63, 94, 156, 244, 152, 190, 137, 146, 67, 41, 50, 84, 161, 139, 230, 247, 234, 223, 223, 80, 172, 62, 163, 53, 81, 162, 190, 106, 89, 34, 238, 187, 133, 4, 131, 195, 163, 221, 27, 227, 231, 189, 8, 153, 142, 137, 118, 0, 144, 124, 101, 104, 14, 119], index: 0 } +[2025-11-18T04:07:30.367815097Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 63773, data_segment = BinString(len=296), data remaining = 1467 +[2025-11-18T04:07:30.367850517Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 63773, buffer length = 296, bytes written = 296 +[2025-11-18T04:07:30.367880493Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.367887678Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.367891291Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.368029411Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 512 bytes +[2025-11-18T04:07:30.368058789Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [113, 234, 27, 32, 83, 239, 165, 127, 228, 129, 35, 216, 119, 156, 12, 52, 182, 135, 68, 235, 78, 105, 16, 151, 208, 75, 115, 123, 195, 209, 205, 173, 188, 123, 138, 41, 24, 230, 5, 226, 49, 210, 105, 38, 251, 40, 237, 78, 93, 10, 180, 33, 163, 36, 200, 142, 218, 88, 44, 24, 156, 107, 53, 39, 151, 180, 78, 240, 24, 169, 189, 142, 208, 186, 107, 0, 4, 52, 157, 138, 144, 177, 85, 164, 194, 57, 186, 52, 19, 84, 66, 243, 212, 220, 69, 223, 198, 87, 59, 166, 101, 17, 118, 177, 182, 164, 58, 141, 76, 98, 61, 2, 156, 97, 83, 189, 160, 233, 239, 235, 252, 68, 151, 106, 63, 18, 247, 139, 116, 13, 136, 63, 93, 42, 146, 111, 60, 73, 146, 112, 43, 199, 92, 166, 218, 147, 184, 60, 23, 38, 66, 104, 84, 222, 155, 98, 39, 81, 177, 170, 82, 147, 191, 212, 239, 58, 210, 141, 64, 101, 83, 24, 11, 71, 17, 100, 8, 232, 222, 37, 132, 218, 132, 204, 37, 89, 0, 208, 60, 205, 186, 143, 171, 76, 141, 123, 34, 26, 8, 199, 107, 57, 43, 194, 208, 225, 46, 125, 7, 45, 222, 43, 16, 112, 52, 109, 229, 28, 90, 143, 60, 181, 125, 24, 84, 119, 26, 36, 73, 12, 38, 13, 5, 163, 99, 45, 209, 100, 219, 228, 33, 204, 235, 109, 217, 209, 203, 39, 224, 121, 156, 154, 168, 78, 225, 57, 130, 148, 68, 85, 228, 76, 156, 184, 237, 37, 186, 230, 222, 133, 232, 192, 56, 20, 170, 130, 101, 0, 151, 225, 89, 183, 54, 253, 152, 178, 74, 226, 12, 8, 35, 240, 72, 49, 150, 212, 159, 233, 159, 57, 59, 27, 104, 248, 1, 169, 119, 179, 171, 128, 206, 94, 49, 155, 211, 200, 163, 185, 157, 196, 149, 161, 75, 198, 53, 98, 38, 190, 222, 250, 123, 16, 203, 175, 109, 8, 82, 43, 172, 30, 195, 126, 193, 14, 75, 214, 219, 208, 126, 166, 91, 151, 88, 88, 37, 195, 136, 59, 88, 122, 95, 236, 25, 145, 164, 136, 71, 51, 48, 86, 65, 226, 213, 19, 110, 233, 76, 81, 99, 255, 108, 157, 7, 240, 217, 219, 166, 50, 81, 93, 139, 183, 90, 76, 109, 144, 63, 158, 245, 22, 187, 221, 251, 220, 176, 195, 183, 207, 89, 79, 180, 121, 243, 111, 58, 179, 166, 171, 110, 62, 207, 167, 9, 183, 220, 49, 158, 232, 76, 15, 48, 249, 42, 247, 164, 209, 229, 90, 86, 38, 203, 145, 241, 40, 238, 17, 59, 47, 199, 146, 111, 200, 42, 207, 33, 106, 121, 248, 198, 162, 25, 52, 94, 34, 158, 218, 98, 185, 90, 222, 35, 131, 170, 193, 90, 202, 155, 38, 108, 84, 60, 166, 195, 124, 104, 104, 26, 78, 124, 141, 120, 86, 228, 214, 9, 250, 136, 225, 14, 23, 145, 67, 43, 39, 218, 43, 11, 170, 135, 85, 122, 108] +[2025-11-18T04:07:30.368118657Z TRACE sunset_sftp::sftphandler::sftphandler] Received 512 bytes to process +[2025-11-18T04:07:30.368126820Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.368132389Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 512 +[2025-11-18T04:07:30.368136826Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [113, 234, 27, 32, 83, 239, 165, 127, 228, 129, 35, 216, 119, 156, 12, 52, 182, 135, 68, 235, 78, 105, 16, 151, 208, 75, 115, 123, 195, 209, 205, 173, 188, 123, 138, 41, 24, 230, 5, 226, 49, 210, 105, 38, 251, 40, 237, 78, 93, 10, 180, 33, 163, 36, 200, 142, 218, 88, 44, 24, 156, 107, 53, 39, 151, 180, 78, 240, 24, 169, 189, 142, 208, 186, 107, 0, 4, 52, 157, 138, 144, 177, 85, 164, 194, 57, 186, 52, 19, 84, 66, 243, 212, 220, 69, 223, 198, 87, 59, 166, 101, 17, 118, 177, 182, 164, 58, 141, 76, 98, 61, 2, 156, 97, 83, 189, 160, 233, 239, 235, 252, 68, 151, 106, 63, 18, 247, 139, 116, 13, 136, 63, 93, 42, 146, 111, 60, 73, 146, 112, 43, 199, 92, 166, 218, 147, 184, 60, 23, 38, 66, 104, 84, 222, 155, 98, 39, 81, 177, 170, 82, 147, 191, 212, 239, 58, 210, 141, 64, 101, 83, 24, 11, 71, 17, 100, 8, 232, 222, 37, 132, 218, 132, 204, 37, 89, 0, 208, 60, 205, 186, 143, 171, 76, 141, 123, 34, 26, 8, 199, 107, 57, 43, 194, 208, 225, 46, 125, 7, 45, 222, 43, 16, 112, 52, 109, 229, 28, 90, 143, 60, 181, 125, 24, 84, 119, 26, 36, 73, 12, 38, 13, 5, 163, 99, 45, 209, 100, 219, 228, 33, 204, 235, 109, 217, 209, 203, 39, 224, 121, 156, 154, 168, 78, 225, 57, 130, 148, 68, 85, 228, 76, 156, 184, 237, 37, 186, 230, 222, 133, 232, 192, 56, 20, 170, 130, 101, 0, 151, 225, 89, 183, 54, 253, 152, 178, 74, 226, 12, 8, 35, 240, 72, 49, 150, 212, 159, 233, 159, 57, 59, 27, 104, 248, 1, 169, 119, 179, 171, 128, 206, 94, 49, 155, 211, 200, 163, 185, 157, 196, 149, 161, 75, 198, 53, 98, 38, 190, 222, 250, 123, 16, 203, 175, 109, 8, 82, 43, 172, 30, 195, 126, 193, 14, 75, 214, 219, 208, 126, 166, 91, 151, 88, 88, 37, 195, 136, 59, 88, 122, 95, 236, 25, 145, 164, 136, 71, 51, 48, 86, 65, 226, 213, 19, 110, 233, 76, 81, 99, 255, 108, 157, 7, 240, 217, 219, 166, 50, 81, 93, 139, 183, 90, 76, 109, 144, 63, 158, 245, 22, 187, 221, 251, 220, 176, 195, 183, 207, 89, 79, 180, 121, 243, 111, 58, 179, 166, 171, 110, 62, 207, 167, 9, 183, 220, 49, 158, 232, 76, 15, 48, 249, 42, 247, 164, 209, 229, 90, 86, 38, 203, 145, 241, 40, 238, 17, 59, 47, 199, 146, 111, 200, 42, 207, 33, 106, 121, 248, 198, 162, 25, 52, 94, 34, 158, 218, 98, 185, 90, 222, 35, 131, 170, 193, 90, 202, 155, 38, 108, 84, 60, 166, 195, 124, 104, 104, 26, 78, 124, 141, 120, 86, 228, 214, 9, 250, 136, 225, 14, 23, 145, 67, 43, 39, 218, 43, 11, 170, 135, 85, 122, 108], index: 0 } +[2025-11-18T04:07:30.368188552Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 64069, data_segment = BinString(len=512), data remaining = 955 +[2025-11-18T04:07:30.368239124Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 64069, buffer length = 512, bytes written = 512 +[2025-11-18T04:07:30.368273546Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.368280690Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.368308998Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.368326868Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 488 bytes +[2025-11-18T04:07:30.368351542Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [30, 162, 101, 210, 19, 161, 7, 70, 0, 249, 69, 185, 69, 174, 89, 60, 224, 5, 231, 15, 112, 230, 188, 13, 163, 229, 95, 109, 190, 203, 13, 27, 149, 186, 49, 213, 60, 21, 55, 117, 175, 176, 199, 127, 150, 103, 170, 98, 236, 108, 53, 0, 141, 149, 147, 93, 30, 186, 241, 62, 42, 179, 53, 182, 36, 238, 104, 247, 219, 14, 185, 174, 221, 254, 204, 253, 131, 239, 54, 168, 40, 199, 247, 66, 128, 198, 239, 181, 197, 147, 226, 6, 120, 94, 13, 254, 249, 34, 67, 37, 150, 162, 148, 142, 101, 255, 66, 49, 175, 41, 95, 2, 200, 122, 30, 225, 183, 169, 178, 245, 128, 88, 10, 100, 60, 24, 43, 206, 157, 96, 40, 148, 162, 205, 4, 188, 161, 220, 38, 223, 44, 198, 203, 128, 211, 105, 63, 123, 101, 112, 182, 242, 44, 61, 118, 77, 83, 169, 254, 246, 133, 204, 39, 72, 217, 137, 107, 146, 7, 247, 146, 91, 227, 66, 136, 217, 78, 0, 93, 65, 241, 147, 89, 28, 115, 188, 115, 16, 51, 174, 222, 174, 7, 207, 29, 196, 179, 165, 14, 73, 246, 22, 255, 207, 174, 142, 188, 254, 252, 52, 108, 126, 207, 235, 250, 54, 76, 11, 160, 131, 89, 51, 232, 136, 251, 233, 190, 177, 246, 250, 5, 153, 215, 16, 133, 94, 31, 76, 173, 115, 95, 75, 176, 165, 58, 235, 133, 181, 238, 47, 28, 60, 226, 101, 24, 173, 147, 104, 5, 227, 135, 76, 94, 106, 89, 4, 255, 221, 149, 10, 39, 206, 227, 219, 248, 118, 178, 227, 170, 98, 192, 119, 43, 181, 99, 4, 18, 120, 43, 247, 109, 147, 82, 119, 91, 52, 12, 98, 144, 170, 8, 33, 247, 97, 115, 76, 89, 177, 171, 51, 167, 206, 176, 199, 142, 104, 191, 248, 168, 202, 231, 87, 47, 63, 90, 123, 80, 82, 44, 249, 130, 173, 50, 78, 162, 252, 178, 58, 201, 104, 45, 132, 214, 141, 103, 128, 22, 149, 7, 186, 109, 250, 200, 53, 28, 157, 139, 158, 163, 235, 211, 242, 148, 110, 73, 230, 37, 193, 202, 56, 210, 44, 224, 44, 120, 199, 217, 248, 91, 224, 119, 213, 1, 30, 145, 5, 201, 170, 228, 231, 175, 137, 56, 64, 71, 126, 255, 195, 200, 0, 250, 25, 12, 82, 108, 136, 146, 163, 50, 142, 151, 120, 161, 201, 165, 164, 22, 223, 164, 194, 154, 165, 217, 75, 231, 79, 125, 233, 109, 155, 160, 245, 105, 121, 28, 199, 169, 155, 227, 222, 244, 79, 86, 49, 134, 30, 102, 194, 104, 238, 17, 210, 62, 225, 78, 104, 43, 139, 59, 83, 5, 9, 115, 149, 208, 135, 109, 189, 248, 227, 45, 51, 243, 135, 78, 236, 38, 99, 206, 132, 217, 195, 9, 227, 65, 137, 89, 116] +[2025-11-18T04:07:30.368409124Z TRACE sunset_sftp::sftphandler::sftphandler] Received 488 bytes to process +[2025-11-18T04:07:30.368416474Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.368420561Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 488 +[2025-11-18T04:07:30.368443330Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [30, 162, 101, 210, 19, 161, 7, 70, 0, 249, 69, 185, 69, 174, 89, 60, 224, 5, 231, 15, 112, 230, 188, 13, 163, 229, 95, 109, 190, 203, 13, 27, 149, 186, 49, 213, 60, 21, 55, 117, 175, 176, 199, 127, 150, 103, 170, 98, 236, 108, 53, 0, 141, 149, 147, 93, 30, 186, 241, 62, 42, 179, 53, 182, 36, 238, 104, 247, 219, 14, 185, 174, 221, 254, 204, 253, 131, 239, 54, 168, 40, 199, 247, 66, 128, 198, 239, 181, 197, 147, 226, 6, 120, 94, 13, 254, 249, 34, 67, 37, 150, 162, 148, 142, 101, 255, 66, 49, 175, 41, 95, 2, 200, 122, 30, 225, 183, 169, 178, 245, 128, 88, 10, 100, 60, 24, 43, 206, 157, 96, 40, 148, 162, 205, 4, 188, 161, 220, 38, 223, 44, 198, 203, 128, 211, 105, 63, 123, 101, 112, 182, 242, 44, 61, 118, 77, 83, 169, 254, 246, 133, 204, 39, 72, 217, 137, 107, 146, 7, 247, 146, 91, 227, 66, 136, 217, 78, 0, 93, 65, 241, 147, 89, 28, 115, 188, 115, 16, 51, 174, 222, 174, 7, 207, 29, 196, 179, 165, 14, 73, 246, 22, 255, 207, 174, 142, 188, 254, 252, 52, 108, 126, 207, 235, 250, 54, 76, 11, 160, 131, 89, 51, 232, 136, 251, 233, 190, 177, 246, 250, 5, 153, 215, 16, 133, 94, 31, 76, 173, 115, 95, 75, 176, 165, 58, 235, 133, 181, 238, 47, 28, 60, 226, 101, 24, 173, 147, 104, 5, 227, 135, 76, 94, 106, 89, 4, 255, 221, 149, 10, 39, 206, 227, 219, 248, 118, 178, 227, 170, 98, 192, 119, 43, 181, 99, 4, 18, 120, 43, 247, 109, 147, 82, 119, 91, 52, 12, 98, 144, 170, 8, 33, 247, 97, 115, 76, 89, 177, 171, 51, 167, 206, 176, 199, 142, 104, 191, 248, 168, 202, 231, 87, 47, 63, 90, 123, 80, 82, 44, 249, 130, 173, 50, 78, 162, 252, 178, 58, 201, 104, 45, 132, 214, 141, 103, 128, 22, 149, 7, 186, 109, 250, 200, 53, 28, 157, 139, 158, 163, 235, 211, 242, 148, 110, 73, 230, 37, 193, 202, 56, 210, 44, 224, 44, 120, 199, 217, 248, 91, 224, 119, 213, 1, 30, 145, 5, 201, 170, 228, 231, 175, 137, 56, 64, 71, 126, 255, 195, 200, 0, 250, 25, 12, 82, 108, 136, 146, 163, 50, 142, 151, 120, 161, 201, 165, 164, 22, 223, 164, 194, 154, 165, 217, 75, 231, 79, 125, 233, 109, 155, 160, 245, 105, 121, 28, 199, 169, 155, 227, 222, 244, 79, 86, 49, 134, 30, 102, 194, 104, 238, 17, 210, 62, 225, 78, 104, 43, 139, 59, 83, 5, 9, 115, 149, 208, 135, 109, 189, 248, 227, 45, 51, 243, 135, 78, 236, 38, 99, 206, 132, 217, 195, 9, 227, 65, 137, 89, 116], index: 0 } +[2025-11-18T04:07:30.368502715Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 64581, data_segment = BinString(len=488), data remaining = 467 +[2025-11-18T04:07:30.368535860Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 64581, buffer length = 488, bytes written = 488 +[2025-11-18T04:07:30.368564271Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.368571476Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.368575131Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.368612126Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 14 bytes +[2025-11-18T04:07:30.368618714Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [79, 151, 215, 196, 156, 81, 173, 114, 208, 142, 112, 168, 70, 47] +[2025-11-18T04:07:30.368623573Z TRACE sunset_sftp::sftphandler::sftphandler] Received 14 bytes to process +[2025-11-18T04:07:30.368627268Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.368654742Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 14 +[2025-11-18T04:07:30.368664325Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [79, 151, 215, 196, 156, 81, 173, 114, 208, 142, 112, 168, 70, 47], index: 0 } +[2025-11-18T04:07:30.368673487Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 65069, data_segment = BinString(len=14), data remaining = 453 +[2025-11-18T04:07:30.368753386Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 65069, buffer length = 14, bytes written = 14 +[2025-11-18T04:07:30.368784236Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.368791421Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.368795086Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.368948431Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 512 bytes +[2025-11-18T04:07:30.368981453Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [51, 48, 211, 251, 123, 62, 184, 143, 102, 28, 100, 189, 149, 133, 208, 99, 112, 51, 90, 49, 127, 117, 113, 251, 150, 32, 222, 249, 164, 238, 182, 58, 200, 251, 63, 166, 129, 42, 16, 27, 155, 118, 111, 156, 170, 83, 147, 96, 16, 135, 228, 202, 225, 207, 227, 129, 111, 153, 59, 137, 93, 120, 155, 127, 165, 32, 45, 126, 42, 180, 161, 130, 173, 137, 0, 44, 152, 71, 153, 187, 88, 70, 44, 90, 194, 229, 205, 226, 107, 35, 228, 4, 72, 26, 44, 6, 130, 85, 74, 95, 23, 120, 210, 44, 142, 67, 230, 192, 251, 64, 222, 179, 17, 39, 11, 90, 184, 62, 199, 118, 197, 103, 238, 203, 0, 210, 218, 4, 110, 109, 195, 180, 149, 174, 50, 85, 200, 146, 71, 64, 119, 229, 224, 204, 67, 93, 30, 53, 39, 78, 55, 169, 2, 152, 160, 181, 186, 69, 150, 131, 49, 220, 50, 22, 128, 181, 28, 133, 159, 111, 3, 212, 222, 173, 248, 6, 78, 94, 175, 110, 166, 116, 14, 243, 172, 48, 194, 85, 156, 219, 89, 252, 47, 247, 26, 137, 141, 27, 226, 10, 188, 247, 103, 245, 192, 145, 215, 119, 119, 223, 169, 18, 42, 245, 18, 213, 77, 70, 212, 128, 70, 111, 102, 202, 83, 216, 77, 2, 146, 161, 189, 143, 5, 25, 44, 22, 44, 31, 20, 15, 69, 174, 91, 168, 251, 133, 150, 245, 76, 196, 42, 0, 183, 253, 192, 173, 190, 216, 98, 113, 14, 251, 131, 91, 40, 139, 113, 45, 252, 171, 143, 27, 164, 57, 63, 165, 234, 86, 114, 105, 93, 68, 27, 92, 53, 255, 127, 21, 247, 250, 170, 246, 28, 186, 163, 119, 57, 13, 86, 194, 6, 142, 179, 104, 227, 235, 143, 149, 25, 90, 184, 157, 140, 240, 203, 77, 148, 139, 129, 210, 169, 73, 83, 248, 95, 147, 146, 132, 68, 4, 179, 189, 190, 156, 140, 8, 117, 132, 218, 190, 113, 11, 122, 87, 179, 143, 127, 5, 217, 191, 236, 249, 206, 50, 197, 13, 18, 20, 48, 194, 110, 185, 88, 238, 80, 139, 81, 105, 182, 17, 8, 99, 219, 195, 69, 136, 150, 70, 243, 102, 180, 79, 85, 79, 232, 164, 62, 16, 184, 194, 10, 138, 219, 17, 121, 214, 218, 128, 197, 242, 166, 78, 162, 8, 242, 214, 227, 163, 87, 90, 212, 139, 153, 182, 117, 201, 31, 241, 131, 123, 150, 8, 130, 40, 111, 147, 203, 15, 229, 228, 33, 232, 39, 48, 85, 178, 154, 158, 92, 28, 69, 161, 56, 9, 7, 252, 171, 208, 98, 144, 55, 171, 20, 0, 0, 4, 25, 6, 0, 0, 0, 12, 0, 0, 0, 4, 215, 124, 9, 14, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 38, 28, 151, 7, 83, 85, 208, 31, 140, 39, 122, 77, 215, 209, 166, 8, 75, 127, 195, 6, 17, 127, 158, 6, 93, 66, 218, 212, 56, 12] +[2025-11-18T04:07:30.369041722Z TRACE sunset_sftp::sftphandler::sftphandler] Received 512 bytes to process +[2025-11-18T04:07:30.369050328Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.369054147Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 512 +[2025-11-18T04:07:30.369076896Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [51, 48, 211, 251, 123, 62, 184, 143, 102, 28, 100, 189, 149, 133, 208, 99, 112, 51, 90, 49, 127, 117, 113, 251, 150, 32, 222, 249, 164, 238, 182, 58, 200, 251, 63, 166, 129, 42, 16, 27, 155, 118, 111, 156, 170, 83, 147, 96, 16, 135, 228, 202, 225, 207, 227, 129, 111, 153, 59, 137, 93, 120, 155, 127, 165, 32, 45, 126, 42, 180, 161, 130, 173, 137, 0, 44, 152, 71, 153, 187, 88, 70, 44, 90, 194, 229, 205, 226, 107, 35, 228, 4, 72, 26, 44, 6, 130, 85, 74, 95, 23, 120, 210, 44, 142, 67, 230, 192, 251, 64, 222, 179, 17, 39, 11, 90, 184, 62, 199, 118, 197, 103, 238, 203, 0, 210, 218, 4, 110, 109, 195, 180, 149, 174, 50, 85, 200, 146, 71, 64, 119, 229, 224, 204, 67, 93, 30, 53, 39, 78, 55, 169, 2, 152, 160, 181, 186, 69, 150, 131, 49, 220, 50, 22, 128, 181, 28, 133, 159, 111, 3, 212, 222, 173, 248, 6, 78, 94, 175, 110, 166, 116, 14, 243, 172, 48, 194, 85, 156, 219, 89, 252, 47, 247, 26, 137, 141, 27, 226, 10, 188, 247, 103, 245, 192, 145, 215, 119, 119, 223, 169, 18, 42, 245, 18, 213, 77, 70, 212, 128, 70, 111, 102, 202, 83, 216, 77, 2, 146, 161, 189, 143, 5, 25, 44, 22, 44, 31, 20, 15, 69, 174, 91, 168, 251, 133, 150, 245, 76, 196, 42, 0, 183, 253, 192, 173, 190, 216, 98, 113, 14, 251, 131, 91, 40, 139, 113, 45, 252, 171, 143, 27, 164, 57, 63, 165, 234, 86, 114, 105, 93, 68, 27, 92, 53, 255, 127, 21, 247, 250, 170, 246, 28, 186, 163, 119, 57, 13, 86, 194, 6, 142, 179, 104, 227, 235, 143, 149, 25, 90, 184, 157, 140, 240, 203, 77, 148, 139, 129, 210, 169, 73, 83, 248, 95, 147, 146, 132, 68, 4, 179, 189, 190, 156, 140, 8, 117, 132, 218, 190, 113, 11, 122, 87, 179, 143, 127, 5, 217, 191, 236, 249, 206, 50, 197, 13, 18, 20, 48, 194, 110, 185, 88, 238, 80, 139, 81, 105, 182, 17, 8, 99, 219, 195, 69, 136, 150, 70, 243, 102, 180, 79, 85, 79, 232, 164, 62, 16, 184, 194, 10, 138, 219, 17, 121, 214, 218, 128, 197, 242, 166, 78, 162, 8, 242, 214, 227, 163, 87, 90, 212, 139, 153, 182, 117, 201, 31, 241, 131, 123, 150, 8, 130, 40, 111, 147, 203, 15, 229, 228, 33, 232, 39, 48, 85, 178, 154, 158, 92, 28, 69, 161, 56, 9, 7, 252, 171, 208, 98, 144, 55, 171, 20, 0, 0, 4, 25, 6, 0, 0, 0, 12, 0, 0, 0, 4, 215, 124, 9, 14, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 38, 28, 151, 7, 83, 85, 208, 31, 140, 39, 122, 77, 215, 209, 166, 8, 75, 127, 195, 6, 17, 127, 158, 6, 93, 66, 218, 212, 56, 12], index: 0 } +[2025-11-18T04:07:30.369135735Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 65083, data_segment = BinString(len=453), data remaining = 0 +[2025-11-18T04:07:30.369175273Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 65083, buffer length = 453, bytes written = 453 +[2025-11-18T04:07:30.369190785Z INFO sunset_sftp::sftphandler::sftphandler] Finished multi part Write Request +[2025-11-18T04:07:30.369213040Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 59 +[2025-11-18T04:07:30.369220102Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 59 +[2025-11-18T04:07:30.369224219Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 59 +[2025-11-18T04:07:30.369231342Z WARN sunset_sftp::sftphandler::sftphandler] RanOut for the SFTP Packet in the source buffer: RanOut +[2025-11-18T04:07:30.369237343Z DEBUG sunset_sftp::sftphandler::sftphandler] Handing Ran out +[2025-11-18T04:07:30.369240936Z DEBUG sunset_sftp::sftphandler::sftphandler] about to decode packet partial write content. Source remaining = 30 +[2025-11-18T04:07:30.369263541Z TRACE sunset_sftp::sftphandler::sftphandler] obscured_file_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, req_id = ReqId(12), offset = 65536, data_in_buffer = BinString(len=30), write_tracker = PartialWriteRequestTracker { req_id: ReqId(12), opaque_handle: DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, remain_data_len: 994, remain_data_offset: 65566 } +[2025-11-18T04:07:30.369340424Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 65536, buffer length = 30, bytes written = 30 +[2025-11-18T04:07:30.369370214Z DEBUG sunset_sftp::sftphandler::sftphandler] Storing a write tracker for a fragmented write request +[2025-11-18T04:07:30.369377760Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.369381640Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.369385377Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.369407447Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.369426902Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 488 bytes +[2025-11-18T04:07:30.369434344Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [43, 55, 242, 47, 60, 147, 5, 18, 108, 226, 171, 31, 250, 134, 222, 88, 76, 18, 140, 139, 149, 232, 117, 190, 108, 79, 56, 233, 54, 103, 151, 155, 205, 115, 56, 64, 125, 215, 131, 193, 19, 235, 187, 62, 122, 202, 67, 106, 202, 176, 8, 166, 97, 192, 98, 233, 183, 176, 101, 151, 34, 94, 88, 158, 124, 92, 59, 162, 124, 42, 84, 163, 159, 153, 89, 183, 17, 80, 90, 208, 121, 44, 98, 90, 234, 161, 214, 153, 182, 129, 199, 61, 48, 225, 184, 134, 84, 31, 218, 9, 70, 10, 169, 22, 148, 205, 229, 245, 191, 103, 33, 248, 164, 79, 183, 60, 97, 40, 205, 135, 36, 92, 210, 36, 51, 48, 222, 160, 34, 171, 94, 3, 107, 196, 119, 104, 214, 186, 93, 2, 198, 137, 161, 252, 237, 103, 31, 152, 123, 147, 68, 38, 116, 172, 152, 98, 63, 112, 225, 112, 195, 216, 142, 18, 15, 201, 222, 138, 9, 189, 255, 56, 244, 113, 202, 217, 52, 223, 59, 67, 216, 147, 137, 167, 28, 253, 149, 78, 161, 231, 117, 151, 2, 5, 12, 90, 247, 201, 243, 237, 236, 64, 229, 53, 198, 248, 250, 87, 113, 247, 237, 50, 232, 95, 140, 16, 231, 6, 182, 157, 250, 57, 128, 217, 42, 0, 82, 218, 88, 238, 182, 96, 105, 6, 245, 0, 184, 139, 121, 85, 43, 144, 23, 164, 136, 15, 224, 9, 127, 228, 148, 104, 196, 14, 79, 187, 139, 128, 100, 102, 4, 15, 155, 22, 13, 133, 116, 67, 192, 249, 126, 96, 133, 14, 98, 22, 124, 17, 36, 207, 206, 195, 143, 180, 54, 31, 111, 190, 193, 250, 19, 70, 47, 86, 70, 170, 227, 164, 9, 186, 26, 3, 148, 1, 83, 111, 234, 57, 144, 198, 82, 141, 15, 65, 239, 71, 85, 182, 87, 45, 19, 252, 18, 244, 212, 40, 170, 155, 124, 71, 109, 242, 40, 198, 39, 75, 230, 215, 179, 76, 186, 192, 165, 34, 87, 155, 108, 22, 129, 62, 178, 239, 88, 229, 113, 194, 133, 134, 108, 239, 161, 90, 234, 184, 100, 124, 58, 124, 189, 14, 195, 242, 88, 170, 182, 224, 239, 54, 117, 94, 71, 240, 9, 0, 240, 249, 36, 80, 180, 134, 9, 139, 22, 4, 71, 132, 42, 95, 103, 132, 203, 60, 33, 214, 1, 33, 116, 98, 71, 168, 49, 166, 103, 103, 221, 147, 159, 128, 54, 147, 110, 3, 163, 122, 106, 118, 239, 224, 233, 153, 28, 227, 22, 195, 141, 7, 131, 239, 164, 148, 97, 52, 17, 181, 126, 252, 187, 188, 62, 27, 36, 28, 29, 81, 173, 28, 31, 245, 127, 214, 172, 170, 104, 116, 4, 45, 176, 188, 148, 148, 177, 84, 21, 159, 161, 92, 159, 107, 40, 119, 130, 160, 70, 129, 209, 192, 106, 72] +[2025-11-18T04:07:30.369488540Z TRACE sunset_sftp::sftphandler::sftphandler] Received 488 bytes to process +[2025-11-18T04:07:30.369495365Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.369499060Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 488 +[2025-11-18T04:07:30.369503363Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [43, 55, 242, 47, 60, 147, 5, 18, 108, 226, 171, 31, 250, 134, 222, 88, 76, 18, 140, 139, 149, 232, 117, 190, 108, 79, 56, 233, 54, 103, 151, 155, 205, 115, 56, 64, 125, 215, 131, 193, 19, 235, 187, 62, 122, 202, 67, 106, 202, 176, 8, 166, 97, 192, 98, 233, 183, 176, 101, 151, 34, 94, 88, 158, 124, 92, 59, 162, 124, 42, 84, 163, 159, 153, 89, 183, 17, 80, 90, 208, 121, 44, 98, 90, 234, 161, 214, 153, 182, 129, 199, 61, 48, 225, 184, 134, 84, 31, 218, 9, 70, 10, 169, 22, 148, 205, 229, 245, 191, 103, 33, 248, 164, 79, 183, 60, 97, 40, 205, 135, 36, 92, 210, 36, 51, 48, 222, 160, 34, 171, 94, 3, 107, 196, 119, 104, 214, 186, 93, 2, 198, 137, 161, 252, 237, 103, 31, 152, 123, 147, 68, 38, 116, 172, 152, 98, 63, 112, 225, 112, 195, 216, 142, 18, 15, 201, 222, 138, 9, 189, 255, 56, 244, 113, 202, 217, 52, 223, 59, 67, 216, 147, 137, 167, 28, 253, 149, 78, 161, 231, 117, 151, 2, 5, 12, 90, 247, 201, 243, 237, 236, 64, 229, 53, 198, 248, 250, 87, 113, 247, 237, 50, 232, 95, 140, 16, 231, 6, 182, 157, 250, 57, 128, 217, 42, 0, 82, 218, 88, 238, 182, 96, 105, 6, 245, 0, 184, 139, 121, 85, 43, 144, 23, 164, 136, 15, 224, 9, 127, 228, 148, 104, 196, 14, 79, 187, 139, 128, 100, 102, 4, 15, 155, 22, 13, 133, 116, 67, 192, 249, 126, 96, 133, 14, 98, 22, 124, 17, 36, 207, 206, 195, 143, 180, 54, 31, 111, 190, 193, 250, 19, 70, 47, 86, 70, 170, 227, 164, 9, 186, 26, 3, 148, 1, 83, 111, 234, 57, 144, 198, 82, 141, 15, 65, 239, 71, 85, 182, 87, 45, 19, 252, 18, 244, 212, 40, 170, 155, 124, 71, 109, 242, 40, 198, 39, 75, 230, 215, 179, 76, 186, 192, 165, 34, 87, 155, 108, 22, 129, 62, 178, 239, 88, 229, 113, 194, 133, 134, 108, 239, 161, 90, 234, 184, 100, 124, 58, 124, 189, 14, 195, 242, 88, 170, 182, 224, 239, 54, 117, 94, 71, 240, 9, 0, 240, 249, 36, 80, 180, 134, 9, 139, 22, 4, 71, 132, 42, 95, 103, 132, 203, 60, 33, 214, 1, 33, 116, 98, 71, 168, 49, 166, 103, 103, 221, 147, 159, 128, 54, 147, 110, 3, 163, 122, 106, 118, 239, 224, 233, 153, 28, 227, 22, 195, 141, 7, 131, 239, 164, 148, 97, 52, 17, 181, 126, 252, 187, 188, 62, 27, 36, 28, 29, 81, 173, 28, 31, 245, 127, 214, 172, 170, 104, 116, 4, 45, 176, 188, 148, 148, 177, 84, 21, 159, 161, 92, 159, 107, 40, 119, 130, 160, 70, 129, 209, 192, 106, 72], index: 0 } +[2025-11-18T04:07:30.369557096Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 65566, data_segment = BinString(len=488), data remaining = 506 +[2025-11-18T04:07:30.369613577Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 65566, buffer length = 488, bytes written = 488 +[2025-11-18T04:07:30.369643882Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.369651088Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.369654845Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.369682967Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 42 bytes +[2025-11-18T04:07:30.369708938Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [115, 168, 108, 51, 193, 52, 19, 31, 170, 36, 253, 14, 65, 50, 49, 88, 211, 185, 67, 14, 180, 123, 6, 104, 161, 36, 215, 59, 30, 39, 183, 67, 103, 153, 2, 76, 248, 171, 77, 162, 79, 167] +[2025-11-18T04:07:30.369740776Z TRACE sunset_sftp::sftphandler::sftphandler] Received 42 bytes to process +[2025-11-18T04:07:30.369748733Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.369752439Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 42 +[2025-11-18T04:07:30.369756762Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [115, 168, 108, 51, 193, 52, 19, 31, 170, 36, 253, 14, 65, 50, 49, 88, 211, 185, 67, 14, 180, 123, 6, 104, 161, 36, 215, 59, 30, 39, 183, 67, 103, 153, 2, 76, 248, 171, 77, 162, 79, 167], index: 0 } +[2025-11-18T04:07:30.369765399Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 66054, data_segment = BinString(len=42), data remaining = 464 +[2025-11-18T04:07:30.369796517Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 66054, buffer length = 42, bytes written = 42 +[2025-11-18T04:07:30.369826502Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.369833800Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.369837475Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.369920432Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 464 bytes +[2025-11-18T04:07:30.369949090Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [144, 252, 138, 11, 49, 93, 92, 70, 91, 200, 15, 151, 171, 209, 193, 224, 198, 108, 107, 33, 29, 247, 54, 131, 42, 129, 145, 69, 168, 65, 246, 87, 69, 140, 137, 171, 22, 223, 208, 133, 172, 54, 208, 168, 11, 51, 51, 179, 124, 207, 104, 39, 26, 140, 189, 192, 72, 134, 18, 86, 242, 95, 119, 47, 243, 30, 227, 180, 119, 13, 139, 55, 115, 92, 38, 251, 204, 89, 227, 98, 170, 32, 2, 125, 129, 13, 111, 180, 77, 82, 18, 191, 146, 148, 39, 244, 34, 36, 15, 90, 60, 66, 118, 143, 150, 42, 246, 59, 152, 69, 178, 174, 173, 41, 41, 68, 17, 226, 96, 133, 142, 228, 252, 133, 80, 178, 183, 235, 54, 61, 148, 149, 158, 75, 241, 91, 229, 204, 120, 136, 66, 74, 158, 61, 38, 251, 175, 36, 236, 179, 192, 212, 53, 193, 162, 84, 244, 35, 85, 102, 36, 144, 242, 62, 10, 150, 161, 26, 156, 177, 244, 27, 58, 164, 187, 51, 126, 214, 2, 200, 186, 136, 58, 175, 113, 104, 38, 80, 155, 57, 11, 74, 72, 98, 199, 24, 249, 108, 59, 102, 33, 160, 171, 168, 13, 144, 143, 16, 65, 55, 135, 101, 233, 213, 228, 138, 189, 140, 16, 90, 33, 198, 133, 32, 183, 7, 154, 61, 57, 43, 241, 29, 43, 191, 50, 246, 249, 50, 177, 21, 49, 237, 228, 249, 224, 33, 213, 95, 81, 35, 23, 33, 208, 114, 41, 183, 18, 19, 185, 2, 127, 224, 158, 228, 32, 244, 0, 38, 240, 149, 51, 113, 46, 55, 17, 35, 50, 39, 139, 115, 238, 30, 74, 26, 226, 56, 57, 178, 159, 49, 61, 204, 98, 27, 123, 37, 131, 102, 179, 61, 36, 221, 15, 14, 51, 100, 221, 89, 122, 82, 172, 98, 143, 114, 159, 181, 153, 235, 235, 94, 217, 5, 92, 123, 66, 186, 40, 184, 153, 74, 229, 109, 251, 39, 206, 18, 151, 65, 191, 233, 187, 105, 71, 197, 178, 252, 152, 113, 179, 236, 147, 105, 8, 123, 95, 238, 190, 244, 251, 11, 87, 114, 178, 3, 240, 183, 3, 171, 171, 17, 213, 63, 74, 239, 129, 235, 127, 198, 35, 210, 87, 233, 28, 225, 194, 57, 27, 50, 1, 181, 87, 118, 206, 140, 130, 230, 134, 190, 166, 172, 98, 229, 152, 112, 114, 177, 122, 102, 255, 55, 31, 52, 28, 237, 35, 205, 67, 103, 159, 224, 134, 181, 42, 59, 142, 12, 30, 121, 177, 101, 146, 78, 249, 246, 175, 107, 205, 154, 36, 230, 89, 16, 110, 26, 87, 247, 195, 243, 39, 245, 190, 176, 241, 153, 183, 18, 146, 245, 68, 239, 209, 23, 63, 62] +[2025-11-18T04:07:30.370005942Z TRACE sunset_sftp::sftphandler::sftphandler] Received 464 bytes to process +[2025-11-18T04:07:30.370012622Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.370016369Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 464 +[2025-11-18T04:07:30.370024100Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [144, 252, 138, 11, 49, 93, 92, 70, 91, 200, 15, 151, 171, 209, 193, 224, 198, 108, 107, 33, 29, 247, 54, 131, 42, 129, 145, 69, 168, 65, 246, 87, 69, 140, 137, 171, 22, 223, 208, 133, 172, 54, 208, 168, 11, 51, 51, 179, 124, 207, 104, 39, 26, 140, 189, 192, 72, 134, 18, 86, 242, 95, 119, 47, 243, 30, 227, 180, 119, 13, 139, 55, 115, 92, 38, 251, 204, 89, 227, 98, 170, 32, 2, 125, 129, 13, 111, 180, 77, 82, 18, 191, 146, 148, 39, 244, 34, 36, 15, 90, 60, 66, 118, 143, 150, 42, 246, 59, 152, 69, 178, 174, 173, 41, 41, 68, 17, 226, 96, 133, 142, 228, 252, 133, 80, 178, 183, 235, 54, 61, 148, 149, 158, 75, 241, 91, 229, 204, 120, 136, 66, 74, 158, 61, 38, 251, 175, 36, 236, 179, 192, 212, 53, 193, 162, 84, 244, 35, 85, 102, 36, 144, 242, 62, 10, 150, 161, 26, 156, 177, 244, 27, 58, 164, 187, 51, 126, 214, 2, 200, 186, 136, 58, 175, 113, 104, 38, 80, 155, 57, 11, 74, 72, 98, 199, 24, 249, 108, 59, 102, 33, 160, 171, 168, 13, 144, 143, 16, 65, 55, 135, 101, 233, 213, 228, 138, 189, 140, 16, 90, 33, 198, 133, 32, 183, 7, 154, 61, 57, 43, 241, 29, 43, 191, 50, 246, 249, 50, 177, 21, 49, 237, 228, 249, 224, 33, 213, 95, 81, 35, 23, 33, 208, 114, 41, 183, 18, 19, 185, 2, 127, 224, 158, 228, 32, 244, 0, 38, 240, 149, 51, 113, 46, 55, 17, 35, 50, 39, 139, 115, 238, 30, 74, 26, 226, 56, 57, 178, 159, 49, 61, 204, 98, 27, 123, 37, 131, 102, 179, 61, 36, 221, 15, 14, 51, 100, 221, 89, 122, 82, 172, 98, 143, 114, 159, 181, 153, 235, 235, 94, 217, 5, 92, 123, 66, 186, 40, 184, 153, 74, 229, 109, 251, 39, 206, 18, 151, 65, 191, 233, 187, 105, 71, 197, 178, 252, 152, 113, 179, 236, 147, 105, 8, 123, 95, 238, 190, 244, 251, 11, 87, 114, 178, 3, 240, 183, 3, 171, 171, 17, 213, 63, 74, 239, 129, 235, 127, 198, 35, 210, 87, 233, 28, 225, 194, 57, 27, 50, 1, 181, 87, 118, 206, 140, 130, 230, 134, 190, 166, 172, 98, 229, 152, 112, 114, 177, 122, 102, 255, 55, 31, 52, 28, 237, 35, 205, 67, 103, 159, 224, 134, 181, 42, 59, 142, 12, 30, 121, 177, 101, 146, 78, 249, 246, 175, 107, 205, 154, 36, 230, 89, 16, 110, 26, 87, 247, 195, 243, 39, 245, 190, 176, 241, 153, 183, 18, 146, 245, 68, 239, 209, 23, 63, 62], index: 0 } +[2025-11-18T04:07:30.370074055Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 66096, data_segment = BinString(len=464), data remaining = 0 +[2025-11-18T04:07:30.370107550Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 66096, buffer length = 464, bytes written = 464 +[2025-11-18T04:07:30.370140253Z INFO sunset_sftp::sftphandler::sftphandler] Finished multi part Write Request +[2025-11-18T04:07:30.370166348Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.370193647Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.370220554Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.414834521Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:07:30.414888388Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 9, 0, 0, 0, 4, 215, 124, 9, 14] +[2025-11-18T04:07:30.414897930Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:07:30.414901863Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.414905599Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:07:30.414910478Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:07:30.414941452Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(9), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:07:30.414973723Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/65kB_random" was successful +[2025-11-18T04:07:30.415018140Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.415049062Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.415056278Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.415060097Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.415629821Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 0 bytes +[2025-11-18T04:07:30.415667815Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [] +[2025-11-18T04:07:30.415675515Z DEBUG sunset_sftp::sftphandler::sftphandler] client disconnected +[2025-11-18T04:07:30.415679293Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing returned: Err(ClientDisconnected) +[2025-11-18T04:07:30.415686128Z WARN sunset_demo_sftp_std] sftp_loop finished: Err(ChannelEOF) +[2025-11-18T04:07:30.415693045Z WARN sunset_demo_common::server] Ended with error ChannelEOF +[2025-11-18T04:07:30.415799060Z INFO sunset_demo_common::server] Listening on TCP:22... +[2025-11-18T04:07:30.444787113Z INFO sunset_demo_common::server] Connection from 192.168.69.100:45016 +[2025-11-18T04:07:30.444866539Z INFO sunset_demo_sftp_std] prog_loop started +[2025-11-18T04:07:30.473277794Z INFO sunset_demo_common::server] Allowing auth for user any +[2025-11-18T04:07:30.475576502Z WARN sunset::channel] Unknown channel req type "env" +[2025-11-18T04:07:30.475678996Z INFO sunset_demo_sftp_std] Starting 'sftp' subsystem +[2025-11-18T04:07:30.475762416Z INFO sunset_demo_sftp_std] SFTP loop has received a channel handle ChanNum(0) +[2025-11-18T04:07:30.475823602Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.475911746Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 9 bytes +[2025-11-18T04:07:30.475967919Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 5, 1, 0, 0, 0, 3] +[2025-11-18T04:07:30.476025265Z TRACE sunset_sftp::sftphandler::sftphandler] Received 9 bytes to process +[2025-11-18T04:07:30.476041055Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.476099441Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Initializing ]=======================> Buffer remaining: 9 +[2025-11-18T04:07:30.476118824Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 9 +[2025-11-18T04:07:30.476173978Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.476229100Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.476416003Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.522213652Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 14 bytes +[2025-11-18T04:07:30.522288240Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 10, 16, 0, 0, 0, 1, 0, 0, 0, 1, 46] +[2025-11-18T04:07:30.522311205Z TRACE sunset_sftp::sftphandler::sftphandler] Received 14 bytes to process +[2025-11-18T04:07:30.522320047Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.522328292Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 14 +[2025-11-18T04:07:30.522381006Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 14 +[2025-11-18T04:07:30.522442943Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: PathInfo(ReqId(1), PathInfo { path: TextString(".") }) +[2025-11-18T04:07:30.522463562Z INFO sunset_demo_sftp_std::demosftpserver] finding path for: "." +[2025-11-18T04:07:30.522478652Z DEBUG sunset_demo_sftp_std::demosftpserver] Will return: NameEntry { filename: Filename(TextString("./demo/sftp/std/testing/out/")), _longname: Filename(TextString("")), attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } } +[2025-11-18T04:07:30.522501411Z DEBUG sunset_sftp::sftphandler::sftphandler] PathInfo encoded length: 40 +[2025-11-18T04:07:30.522514309Z TRACE sunset_sftp::sftphandler::sftphandler] PathInfo Response content: 40 +[2025-11-18T04:07:30.522528391Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.522574733Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.522590472Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.522598738Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.523406710Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.523448306Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 2, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.523460638Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.523464519Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.523468214Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.523495215Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.523525118Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(2), LStat { file_path: TextString("./demo/sftp/std/testing/out/./512B_random") }) +[2025-11-18T04:07:30.523539622Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./512B_random" +[2025-11-18T04:07:30.523563112Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./512B_random" is Attrs { size: Some(512), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438849), ext_count: None } +[2025-11-18T04:07:30.523593499Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.523600653Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.523604276Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.523607889Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.524163861Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.524204675Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 3, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.524217059Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.524221104Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.524225057Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.524230369Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.524260478Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(3), Stat { file_path: TextString("./demo/sftp/std/testing/out/./512B_random") }) +[2025-11-18T04:07:30.524295311Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./512B_random" +[2025-11-18T04:07:30.524328292Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./512B_random" is Attrs { size: Some(512), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438849), ext_count: None } +[2025-11-18T04:07:30.524358340Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.524365411Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.524370023Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.524374789Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.525025730Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:07:30.525065330Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 4, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:07:30.525077909Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:07:30.525081810Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.525085454Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:07:30.525089849Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:07:30.525119032Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(4), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./512B_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:07:30.525154628Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./512B_random", mode = SSH_FXF_READ +[2025-11-18T04:07:30.525181473Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:07:30.525242556Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./512B_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }) +[2025-11-18T04:07:30.525273242Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.525280591Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.525284276Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.525306634Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.526078773Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:07:30.526119083Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 5, 0, 0, 0, 4, 55, 15, 63, 111, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:07:30.526128976Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:07:30.526132887Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.526136562Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.526141256Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.526147288Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(5), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:07:30.526176018Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }, filepath = "./demo/sftp/std/testing/out/./512B_random", offset = 0, len = 32768 +[2025-11-18T04:07:30.526215443Z WARN sunset_demo_sftp_std::demosftpserver] Read operation: length + offset > file length. Clipping ( 32768 + 0 > 512) +[2025-11-18T04:07:30.526226241Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 512 +[2025-11-18T04:07:30.526258728Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:07:30.526282496Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.526289660Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.526293808Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.526322569Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.570690246Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:07:30.570758411Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 6, 0, 0, 0, 4, 55, 15, 63, 111, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 126, 0] +[2025-11-18T04:07:30.570779338Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:07:30.570788046Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.570837775Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.570858259Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.570869366Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(6), Read { handle: FileHandle(BinString(len=4)), offset: 512, len: 32256 }) +[2025-11-18T04:07:30.570922039Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }, filepath = "./demo/sftp/std/testing/out/./512B_random", offset = 512, len = 32256 +[2025-11-18T04:07:30.570998398Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF +[2025-11-18T04:07:30.571046304Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.571060160Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.571064617Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.571069197Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.571566825Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:07:30.571598231Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 7, 0, 0, 0, 4, 55, 15, 63, 111] +[2025-11-18T04:07:30.571607269Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:07:30.571611077Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.571614721Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:07:30.571619343Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:07:30.571644779Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(7), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:07:30.571674342Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./512B_random" was successful +[2025-11-18T04:07:30.571691172Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.571714930Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.571722568Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.571726305Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.572166370Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.572196334Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 8, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.572207863Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.572211713Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.572215378Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.572219475Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.572244354Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(8), LStat { file_path: TextString("./demo/sftp/std/testing/out/./16kB_random") }) +[2025-11-18T04:07:30.572273908Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./16kB_random" +[2025-11-18T04:07:30.572315957Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./16kB_random" is Attrs { size: Some(16384), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438849), ext_count: None } +[2025-11-18T04:07:30.572350204Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.572357379Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.572361074Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.572383896Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.572882758Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.572913485Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 9, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.572924705Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.572928544Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.572932209Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.572936326Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.572961597Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(9), Stat { file_path: TextString("./demo/sftp/std/testing/out/./16kB_random") }) +[2025-11-18T04:07:30.572990306Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./16kB_random" +[2025-11-18T04:07:30.573027240Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./16kB_random" is Attrs { size: Some(16384), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438849), ext_count: None } +[2025-11-18T04:07:30.573057524Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.573064997Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.573090289Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.573098112Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.573531692Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:07:30.573561255Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 10, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:07:30.573573361Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:07:30.573577241Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.573580916Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:07:30.573585034Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:07:30.573609934Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(10), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./16kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:07:30.573641103Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./16kB_random", mode = SSH_FXF_READ +[2025-11-18T04:07:30.573668485Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:07:30.573713365Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./16kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }) +[2025-11-18T04:07:30.573744678Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.573751905Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.573755600Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.573778133Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.574301062Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:07:30.574331840Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 11, 0, 0, 0, 4, 204, 179, 134, 195, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:07:30.574341609Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:07:30.574345582Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.574349267Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.574353395Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.574378398Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(11), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:07:30.574407169Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }, filepath = "./demo/sftp/std/testing/out/./16kB_random", offset = 0, len = 32768 +[2025-11-18T04:07:30.574441725Z WARN sunset_demo_sftp_std::demosftpserver] Read operation: length + offset > file length. Clipping ( 32768 + 0 > 16384) +[2025-11-18T04:07:30.574468633Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 16384 +[2025-11-18T04:07:30.620633925Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:07:30.620676819Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.620684745Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.620689017Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.620692846Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.621652731Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:07:30.621688409Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 12, 0, 0, 0, 4, 204, 179, 134, 195, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 64, 0] +[2025-11-18T04:07:30.621699990Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:07:30.621703942Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.621707792Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.621731663Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.621760712Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(12), Read { handle: FileHandle(BinString(len=4)), offset: 16384, len: 16384 }) +[2025-11-18T04:07:30.621790399Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }, filepath = "./demo/sftp/std/testing/out/./16kB_random", offset = 16384, len = 16384 +[2025-11-18T04:07:30.621810307Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF +[2025-11-18T04:07:30.621836051Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.621861816Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.621888745Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.621914201Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.622335882Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:07:30.622365929Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 13, 0, 0, 0, 4, 204, 179, 134, 195] +[2025-11-18T04:07:30.622374833Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:07:30.622378662Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.622382296Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:07:30.622386444Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:07:30.622410665Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(13), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:07:30.622439848Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./16kB_random" was successful +[2025-11-18T04:07:30.622476442Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.622486149Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.622509031Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.622517318Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.622914952Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.622945092Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 14, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.622956673Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.622960523Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.622964177Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.622968335Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.622992855Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(14), LStat { file_path: TextString("./demo/sftp/std/testing/out/./64kB_random") }) +[2025-11-18T04:07:30.623021646Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./64kB_random" +[2025-11-18T04:07:30.623062553Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./64kB_random" is Attrs { size: Some(65536), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438850), ext_count: None } +[2025-11-18T04:07:30.623096893Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.623104160Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.623126127Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.623132911Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.623499520Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.623532913Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 15, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.623544112Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.623547952Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.623571576Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.623598504Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.623625278Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(15), Stat { file_path: TextString("./demo/sftp/std/testing/out/./64kB_random") }) +[2025-11-18T04:07:30.623654831Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./64kB_random" +[2025-11-18T04:07:30.623672289Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./64kB_random" is Attrs { size: Some(65536), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438850), ext_count: None } +[2025-11-18T04:07:30.623683067Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.623707988Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.623715018Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.623719836Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.624113147Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:07:30.624143163Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 16, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:07:30.624154744Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:07:30.624178995Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.624205512Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:07:30.624231473Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:07:30.624260243Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(16), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./64kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:07:30.624274058Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./64kB_random", mode = SSH_FXF_READ +[2025-11-18T04:07:30.624299061Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:07:30.624321872Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./64kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }) +[2025-11-18T04:07:30.624347946Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.624355295Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.624380669Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.624388369Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.624962890Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:07:30.624993740Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 17, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:07:30.625003138Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:07:30.625007019Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.625010684Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.625014811Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.625040237Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(17), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:07:30.625068658Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 0, len = 32768 +[2025-11-18T04:07:30.625104974Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:07:30.676397692Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:07:30.676439834Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.676447544Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.676451755Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.676455543Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.677366008Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 58 bytes +[2025-11-18T04:07:30.677396302Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 18, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 25, 5, 0, 0, 0, 19, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:07:30.677409509Z TRACE sunset_sftp::sftphandler::sftphandler] Received 58 bytes to process +[2025-11-18T04:07:30.677413359Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.677416982Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 58 +[2025-11-18T04:07:30.677421337Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 58 +[2025-11-18T04:07:30.677427328Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(18), Read { handle: FileHandle(BinString(len=4)), offset: 32768, len: 32768 }) +[2025-11-18T04:07:30.677434286Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 32768, len = 32768 +[2025-11-18T04:07:30.677449150Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:07:30.725418534Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:07:30.725464310Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 29 bytes +[2025-11-18T04:07:30.725473574Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 29 +[2025-11-18T04:07:30.725477558Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.725482005Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.725488088Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(19), Read { handle: FileHandle(BinString(len=4)), offset: 65536, len: 32768 }) +[2025-11-18T04:07:30.725516324Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 65536, len = 32768 +[2025-11-18T04:07:30.725556500Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF +[2025-11-18T04:07:30.725565167Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.725569089Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.725572733Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.725579620Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.726220195Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:07:30.726251694Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 20, 0, 0, 0, 4, 195, 17, 206, 102] +[2025-11-18T04:07:30.726260701Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:07:30.726264551Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.726268195Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:07:30.726272703Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:07:30.726277840Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(20), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:07:30.726284839Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./64kB_random" was successful +[2025-11-18T04:07:30.726296965Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.726321732Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.726328927Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.726332695Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.726754056Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.726784927Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 21, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.726797125Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.726800944Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.726804598Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.726808736Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.726813677Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(21), LStat { file_path: TextString("./demo/sftp/std/testing/out/./65kB_random") }) +[2025-11-18T04:07:30.726819792Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./65kB_random" +[2025-11-18T04:07:30.726835911Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./65kB_random" is Attrs { size: Some(66560), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438850), ext_count: None } +[2025-11-18T04:07:30.726866185Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.726873401Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.726877014Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.726880586Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.727295586Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:07:30.727327949Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 22, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:07:30.727339756Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:07:30.727343616Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.727347281Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:07:30.727351367Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:07:30.727355948Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(22), Stat { file_path: TextString("./demo/sftp/std/testing/out/./65kB_random") }) +[2025-11-18T04:07:30.727361867Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./65kB_random" +[2025-11-18T04:07:30.727374559Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./65kB_random" is Attrs { size: Some(66560), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438850), ext_count: None } +[2025-11-18T04:07:30.727383175Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.727407025Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.727414138Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.727417834Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.727764813Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:07:30.727795509Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 23, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:07:30.727806996Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:07:30.727810815Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.727814500Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:07:30.727818597Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:07:30.727842664Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(23), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./65kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:07:30.727874523Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./65kB_random", mode = SSH_FXF_READ +[2025-11-18T04:07:30.727900906Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:07:30.727944983Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./65kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }) +[2025-11-18T04:07:30.727974310Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.727981587Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.727985272Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.728007435Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.728479142Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:07:30.728509478Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 24, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:07:30.728518732Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:07:30.728522674Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.728526339Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.728530559Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.728535521Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(24), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:07:30.728561914Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 0, len = 32768 +[2025-11-18T04:07:30.728626919Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:07:30.822241569Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:07:30.822286676Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.822294478Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.822298544Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.822302353Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:07:30.823271801Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:07:30.823302878Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 25, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0] +[2025-11-18T04:07:30.823314571Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:07:30.823318535Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:07:30.823322230Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:07:30.823327017Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:07:30.823333265Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(25), Read { handle: FileHandle(BinString(len=4)), offset: 32768, len: 32768 }) +[2025-11-18T04:07:30.823361233Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 32768, len = 32768 +[2025-11-18T04:07:30.823399700Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:07:30.868777413Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:07:30.868822489Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:07:30.868830405Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:07:30.868834574Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:07:30.868838310Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel \ No newline at end of file diff --git a/demo/sftp/std/testing/out/get-deadlock-client-killed.log b/demo/sftp/std/testing/out/get-deadlock-client-killed.log new file mode 100644 index 00000000..b737b22f --- /dev/null +++ b/demo/sftp/std/testing/out/get-deadlock-client-killed.log @@ -0,0 +1,55 @@ +Server: + +[2025-11-18T04:08:59.480314646Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:08:59.481255533Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:08:59.481288052Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 18, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0] +[2025-11-18T04:08:59.481298059Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:08:59.481322710Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:08:59.481347629Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:08:59.481375365Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:08:59.481403061Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(18), Read { handle: FileHandle(BinString(len=4)), offset: 32768, len: 32768 }) +[2025-11-18T04:08:59.481434849Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 32768, len = 32768 +[2025-11-18T04:08:59.481473199Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:08:59.530448787Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:08:59.530485451Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:08:59.530493205Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:08:59.530497236Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:08:59.530500918Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:09:04.048434784Z WARN sunset_async::async_sunset] rx complete () +[2025-11-18T04:09:04.048557097Z INFO sunset_demo_common::server] Listening on TCP:22... + +Client: + +sftp> get ./64kB_random +debug3: Looking up ./demo/sftp/std/testing/out/./64kB_random +debug3: Sent message fd 3 T:7 I:14 +debug3: Received stat reply T:105 I:14 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./64kB_random to 64kB_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./64kB_random" to local "64kB_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./64kB_random" +debug3: Sent message fd 3 T:17 I:15 +debug3: Received stat reply T:105 I:15 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./64kB_random" +debug3: Sent remote message SSH2_FXP_OPEN I:16 P:./demo/sftp/std/testing/out/./64kB_random M:0x0001 +64kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug3: Received reply T:103 I:17 R:1 +debug3: Received data 0 -> 32767 +debug3: Request range 32768 -> 65535 (0/2) +debug3: Request range 65536 -> 98303 (1/2) +debug2: channel 0: rcvd adjust 534 +debug3: Received reply T:103 I:18 R:2 +debug3: Received data 32768 -> 65535 +debug3: Finish at 98304 ( 1) +64kB_random 100% 64KB 16.0KB/s 00:04 debug3: send packet: type 1 +debug1: channel 0: free: client-session, nchannels 1 +debug3: channel 0: status: The following connections are open: + #0 client-session (t4 r0 i0/0 o0/0 e[write]/0 fd 4/5/6 sock -1 cc -1 io 0x01/0x02) + +Killed by signal 15. +DOWNLOAD Test Results: +============= +Download PASS: 512B_random +Download PASS: 16kB_random +Download PASS: 64kB_random +Download FAIL: 65kB_random +Cleaning up local files... \ No newline at end of file diff --git a/demo/sftp/std/testing/out/success-get-client.log b/demo/sftp/std/testing/out/success-get-client.log new file mode 100644 index 00000000..6dd21981 --- /dev/null +++ b/demo/sftp/std/testing/out/success-get-client.log @@ -0,0 +1,308 @@ +Testing Multiple GETs... +Generating random data files... +Uploading files to any@192.168.69.2... +Connected to 192.168.69.2. +sftp> put ./512B_random +Uploading ./512B_random to ./demo/sftp/std/testing/out/512B_random +512B_random 100% 512 522.1KB/s 00:00 +sftp> put ./16kB_random +Uploading ./16kB_random to ./demo/sftp/std/testing/out/16kB_random +16kB_random 100% 16KB 230.4KB/s 00:00 +sftp> put ./64kB_random +Uploading ./64kB_random to ./demo/sftp/std/testing/out/64kB_random +64kB_random 100% 64KB 214.0KB/s 00:00 +sftp> put ./65kB_random +Uploading ./65kB_random to ./demo/sftp/std/testing/out/65kB_random +65kB_random 100% 65KB 223.5KB/s 00:00 +sftp> +sftp> bye +client_loop: send disconnect: Broken pipe +UPLOAD Test Results: +============= +Upload PASS: 512B_random +Upload PASS: 16kB_random +Upload PASS: 64kB_random +Upload PASS: 65kB_random +Cleaning up original files... +OpenSSH_8.9p1 Ubuntu-3ubuntu0.13, OpenSSL 3.0.2 15 Mar 2022 +debug1: Reading configuration data /home/jubeor/.ssh/config +debug1: Reading configuration data /etc/ssh/ssh_config +debug1: /etc/ssh/ssh_config line 19: include /etc/ssh/ssh_config.d/*.conf matched no files +debug1: /etc/ssh/ssh_config line 21: Applying options for * +debug2: resolve_canonicalize: hostname 192.168.69.2 is address +debug3: ssh_connect_direct: entering +debug1: Connecting to 192.168.69.2 [192.168.69.2] port 22. +debug3: set_sock_tos: set socket 3 IP_TOS 0x10 +debug1: Connection established. +debug1: identity file /home/jubeor/.ssh/id_rsa type 0 +debug1: identity file /home/jubeor/.ssh/id_rsa-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa_sk type -1 +debug1: identity file /home/jubeor/.ssh/id_ecdsa_sk-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519 type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519_sk type -1 +debug1: identity file /home/jubeor/.ssh/id_ed25519_sk-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_xmss type -1 +debug1: identity file /home/jubeor/.ssh/id_xmss-cert type -1 +debug1: identity file /home/jubeor/.ssh/id_dsa type -1 +debug1: identity file /home/jubeor/.ssh/id_dsa-cert type -1 +debug1: Local version string SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.13 +debug1: Remote protocol version 2.0, remote software version Sunset-1 +debug1: compat_banner: no match: Sunset-1 +debug2: fd 3 setting O_NONBLOCK +debug1: Authenticating to 192.168.69.2:22 as 'any' +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory +debug3: order_hostkeyalgs: no algorithms matched; accept original +debug3: send packet: type 20 +debug1: SSH2_MSG_KEXINIT sent +debug3: receive packet: type 20 +debug1: SSH2_MSG_KEXINIT received +debug2: local client KEXINIT proposal +debug2: KEX algorithms: curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,sntrup761x25519-sha512@openssh.com,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,ext-info-c,kex-strict-c-v00@openssh.com +debug2: host key algorithms: ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,sk-ecdsa-sha2-nistp256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256 +debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com +debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com +debug2: MACs ctos: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 +debug2: MACs stoc: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 +debug2: compression ctos: none,zlib@openssh.com,zlib +debug2: compression stoc: none,zlib@openssh.com,zlib +debug2: languages ctos: +debug2: languages stoc: +debug2: first_kex_follows 0 +debug2: reserved 0 +debug2: peer server KEXINIT proposal +debug2: KEX algorithms: mlkem768x25519-sha256,curve25519-sha256,curve25519-sha256@libssh.org,kex-strict-s-v00@openssh.com,kexguess2@matt.ucc.asn.au +debug2: host key algorithms: ssh-ed25519,rsa-sha2-256 +debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes256-ctr +debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes256-ctr +debug2: MACs ctos: hmac-sha2-256 +debug2: MACs stoc: hmac-sha2-256 +debug2: compression ctos: none +debug2: compression stoc: none +debug2: languages ctos: +debug2: languages stoc: +debug2: first_kex_follows 0 +debug2: reserved 0 +debug3: kex_choose_conf: will use strict KEX ordering +debug1: kex: algorithm: curve25519-sha256 +debug1: kex: host key algorithm: ssh-ed25519 +debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: compression: none +debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: compression: none +debug3: send packet: type 30 +debug1: expecting SSH2_MSG_KEX_ECDH_REPLY +debug3: receive packet: type 31 +debug1: SSH2_MSG_KEX_ECDH_REPLY received +debug1: Server host key: ssh-ed25519 SHA256:624v6xNDiTpLgI28RnlKgSBDHRrekRUjQFY9rgia+fg +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory +debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory +Warning: Permanently added '192.168.69.2' (ED25519) to the list of known hosts. +debug3: send packet: type 21 +debug1: ssh_packet_send2_wrapped: resetting send seqnr 3 +debug2: ssh_set_newkeys: mode 1 +debug1: rekey out after 134217728 blocks +debug1: SSH2_MSG_NEWKEYS sent +debug1: expecting SSH2_MSG_NEWKEYS +debug3: receive packet: type 21 +debug1: ssh_packet_read_poll2: resetting read seqnr 3 +debug1: SSH2_MSG_NEWKEYS received +debug2: ssh_set_newkeys: mode 0 +debug1: rekey in after 134217728 blocks +debug1: Will attempt key: /home/jubeor/.ssh/id_rsa RSA SHA256:Wc43s4t4VZ5DkRa7At3RMd9E0u/UH4CV75mWxI0G8A4 +debug1: Will attempt key: /home/jubeor/.ssh/id_ecdsa +debug1: Will attempt key: /home/jubeor/.ssh/id_ecdsa_sk +debug1: Will attempt key: /home/jubeor/.ssh/id_ed25519 +debug1: Will attempt key: /home/jubeor/.ssh/id_ed25519_sk +debug1: Will attempt key: /home/jubeor/.ssh/id_xmss +debug1: Will attempt key: /home/jubeor/.ssh/id_dsa +debug2: pubkey_prepare: done +debug3: send packet: type 5 +debug3: receive packet: type 7 +debug1: SSH2_MSG_EXT_INFO received +debug1: kex_input_ext_info: server-sig-algs= +debug3: receive packet: type 6 +debug2: service_accept: ssh-userauth +debug1: SSH2_MSG_SERVICE_ACCEPT received +debug3: send packet: type 50 +debug3: receive packet: type 52 +Authenticated to 192.168.69.2 ([192.168.69.2]:22) using "none". +debug1: channel 0: new [client-session] +debug3: ssh_session2_open: channel_new: 0 +debug2: channel 0: send open +debug3: send packet: type 90 +debug1: Entering interactive session. +debug1: pledge: network +debug3: receive packet: type 91 +debug2: channel_input_open_confirmation: channel 0: callback start +debug2: fd 3 setting TCP_NODELAY +debug3: set_sock_tos: set socket 3 IP_TOS 0x08 +debug2: client_session2_setup: id 0 +debug1: Sending environment. +debug3: Ignored env SHELL +debug3: Ignored env USER_ZDOTDIR +debug3: Ignored env COLORTERM +debug3: Ignored env WSL2_GUI_APPS_ENABLED +debug3: Ignored env TERM_PROGRAM_VERSION +debug3: Ignored env WSL_DISTRO_NAME +debug3: Ignored env NAME +debug3: Ignored env PWD +debug3: Ignored env NIX_PROFILES +debug3: Ignored env LOGNAME +debug3: Ignored env VSCODE_GIT_ASKPASS_NODE +debug3: Ignored env VSCODE_INJECTION +debug3: Ignored env HOME +debug1: channel 0: setting env LANG = "C.UTF-8" +debug2: channel 0: request env confirm 0 +debug3: send packet: type 98 +debug3: Ignored env WSL_INTEROP +debug3: Ignored env LS_COLORS +debug3: Ignored env WAYLAND_DISPLAY +debug3: Ignored env NIX_SSL_CERT_FILE +debug3: Ignored env GIT_ASKPASS +debug3: Ignored env VSCODE_GIT_ASKPASS_EXTRA_ARGS +debug3: Ignored env VSCODE_PYTHON_AUTOACTIVATE_GUARD +debug3: Ignored env TERM +debug3: Ignored env ZDOTDIR +debug3: Ignored env USER +debug3: Ignored env VSCODE_GIT_IPC_HANDLE +debug3: Ignored env DISPLAY +debug3: Ignored env SHLVL +debug3: Ignored env XDG_RUNTIME_DIR +debug3: Ignored env WSLENV +debug3: Ignored env VSCODE_GIT_ASKPASS_MAIN +debug3: Ignored env XDG_DATA_DIRS +debug3: Ignored env PATH +debug3: Ignored env DBUS_SESSION_BUS_ADDRESS +debug3: Ignored env HOSTTYPE +debug3: Ignored env PULSE_SERVER +debug3: Ignored env OLDPWD +debug3: Ignored env TERM_PROGRAM +debug3: Ignored env VSCODE_IPC_HOOK_CLI +debug3: Ignored env _ +debug1: Sending subsystem: sftp +debug2: channel 0: request subsystem confirm 1 +debug3: send packet: type 98 +debug2: channel_input_open_confirmation: channel 0: callback done +debug2: channel 0: open confirm rwindow 1000 rmax 1000 +debug3: receive packet: type 99 +debug2: channel_input_status_confirm: type 99 id 0 +debug2: subsystem request accepted on channel 0 +debug2: Remote version: 3 +Connected to 192.168.69.2. +debug2: Sending SSH2_FXP_REALPATH "." +debug3: Sent message fd 3 T:16 I:1 +debug3: SSH2_FXP_REALPATH . -> ./demo/sftp/std/testing/out/ +sftp> get ./512B_random +debug3: Looking up ./demo/sftp/std/testing/out/./512B_random +debug3: Sent message fd 3 T:7 I:2 +debug3: Received stat reply T:105 I:2 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./512B_random to 512B_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./512B_random" to local "512B_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./512B_random" +debug3: Sent message fd 3 T:17 I:3 +debug3: Received stat reply T:105 I:3 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./512B_random" +debug3: Sent remote message SSH2_FXP_OPEN I:4 P:./demo/sftp/std/testing/out/./512B_random M:0x0001 +512B_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug3: Received reply T:103 I:5 R:1 +debug3: Received data 0 -> 511 +debug3: Short data block, re-requesting 512 -> 32767 ( 1) +debug3: Finish at 32768 ( 1) +debug3: Received reply T:101 I:6 R:1 +512B_random 100% 512 10.8KB/s 00:00 +debug3: Sent message SSH2_FXP_CLOSE I:7 +debug3: SSH2_FXP_STATUS 0 +sftp> get ./16kB_random +debug3: Looking up ./demo/sftp/std/testing/out/./16kB_random +debug3: Sent message fd 3 T:7 I:8 +debug3: Received stat reply T:105 I:8 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./16kB_random to 16kB_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./16kB_random" to local "16kB_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./16kB_random" +debug3: Sent message fd 3 T:17 I:9 +debug2: channel 0: rcvd adjust 516 +debug3: Received stat reply T:105 I:9 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./16kB_random" +debug3: Sent remote message SSH2_FXP_OPEN I:10 P:./demo/sftp/std/testing/out/./16kB_random M:0x0001 +16kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug3: Received reply T:103 I:11 R:1 +debug3: Received data 0 -> 16383 +debug3: Short data block, re-requesting 16384 -> 32767 ( 1) +debug3: Finish at 32768 ( 1) +debug3: Received reply T:101 I:12 R:1 +16kB_random 100% 16KB 333.6KB/s 00:00 +debug3: Sent message SSH2_FXP_CLOSE I:13 +debug3: SSH2_FXP_STATUS 0 +sftp> get ./64kB_random +debug3: Looking up ./demo/sftp/std/testing/out/./64kB_random +debug3: Sent message fd 3 T:7 I:14 +debug3: Received stat reply T:105 I:14 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./64kB_random to 64kB_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./64kB_random" to local "64kB_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./64kB_random" +debug3: Sent message fd 3 T:17 I:15 +debug3: Received stat reply T:105 I:15 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./64kB_random" +debug3: Sent remote message SSH2_FXP_OPEN I:16 P:./demo/sftp/std/testing/out/./64kB_random M:0x0001 +64kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug3: Received reply T:103 I:17 R:1 +debug3: Received data 0 -> 32767 +debug3: Request range 32768 -> 65535 (0/2) +debug3: Request range 65536 -> 98303 (1/2) +debug2: channel 0: rcvd adjust 520 +debug3: Received reply T:103 I:18 R:2 +debug3: Received data 32768 -> 65535 +debug3: Finish at 98304 ( 1) +debug3: Received reply T:101 I:19 R:1 +64kB_random 100% 64KB 623.4KB/s 00:00 +debug3: Sent message SSH2_FXP_CLOSE I:20 +debug3: SSH2_FXP_STATUS 0 +sftp> get ./65kB_random +debug3: Looking up ./demo/sftp/std/testing/out/./65kB_random +debug3: Sent message fd 3 T:7 I:21 +debug3: Received stat reply T:105 I:21 F:0x000f M:100644 +Fetching ./demo/sftp/std/testing/out/./65kB_random to 65kB_random +debug2: do_download: download remote "./demo/sftp/std/testing/out/./65kB_random" to local "65kB_random" +debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./65kB_random" +debug3: Sent message fd 3 T:17 I:22 +debug3: Received stat reply T:105 I:22 F:0x000f M:100644 +debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./65kB_random" +debug3: Sent remote message SSH2_FXP_OPEN I:23 P:./demo/sftp/std/testing/out/./65kB_random M:0x0001 +65kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) +debug2: channel 0: window 1997312 sent adjust 99840 +debug3: Received reply T:103 I:24 R:1 +debug3: Received data 0 -> 32767 +debug3: Request range 32768 -> 65535 (0/2) +debug3: Request range 65536 -> 98303 (1/2) +debug3: Received reply T:103 I:25 R:2 +debug3: Received data 32768 -> 65535 +debug3: Finish at 98304 ( 1) +debug3: Received reply T:103 I:26 R:1 +debug3: Received data 65536 -> 66559 +debug3: Short data block, re-requesting 66560 -> 98303 ( 1) +debug3: Finish at 98304 ( 1) +debug3: Received reply T:101 I:27 R:1 +65kB_random 100% 65KB 87.8KB/s 00:00 +debug3: Sent message SSH2_FXP_CLOSE I:28 +debug3: SSH2_FXP_STATUS 0 +sftp> +sftp> bye +debug2: channel 0: read failed rfd 4 maxlen 946: Broken pipe +debug2: channel 0: read failed +debug2: chan_shutdown_read: channel 0: (i0 o0 sock -1 wfd 4 efd 6 [write]) +debug2: channel 0: input open -> drain +debug2: channel 0: ibuf empty +debug2: channel 0: send eof +debug3: send packet: type 96 +debug2: channel 0: input drain -> closed +debug3: send packet: type 1 +client_loop: send disconnect: Broken pipe +DOWNLOAD Test Results: +============= +Download PASS: 512B_random +Download PASS: 16kB_random +Download PASS: 64kB_random +Download PASS: 65kB_random +Cleaning up local files... \ No newline at end of file diff --git a/demo/sftp/std/testing/out/success-get-server.log b/demo/sftp/std/testing/out/success-get-server.log new file mode 100644 index 00000000..a78cdebb --- /dev/null +++ b/demo/sftp/std/testing/out/success-get-server.log @@ -0,0 +1,351 @@ +[2025-11-18T04:00:55.665183905Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.665245066Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(2), LStat { file_path: TextString("./demo/sftp/std/testing/out/./512B_random") }) +[2025-11-18T04:00:55.665306824Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./512B_random" +[2025-11-18T04:00:55.665390942Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./512B_random" is Attrs { size: Some(512), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438454), ext_count: None } +[2025-11-18T04:00:55.665420750Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.665429106Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.665497840Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.665556398Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.666039369Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:00:55.666069600Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 3, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:00:55.666080764Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:00:55.666084654Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.666088265Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:00:55.666092392Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.666118188Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(3), Stat { file_path: TextString("./demo/sftp/std/testing/out/./512B_random") }) +[2025-11-18T04:00:55.666146165Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./512B_random" +[2025-11-18T04:00:55.666184638Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./512B_random" is Attrs { size: Some(512), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438454), ext_count: None } +[2025-11-18T04:00:55.666220106Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.666227360Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.666231188Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.666234727Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.666614742Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:00:55.666676839Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 4, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:00:55.666693899Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:00:55.666721753Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.666728760Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:00:55.666734903Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:00:55.666740110Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(4), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./512B_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:00:55.666752653Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./512B_random", mode = SSH_FXF_READ +[2025-11-18T04:00:55.666757016Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:00:55.666781248Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./512B_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }) +[2025-11-18T04:00:55.666806961Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.666833560Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.666859870Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.666886644Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.667464094Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:00:55.667505150Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 5, 0, 0, 0, 4, 55, 15, 63, 111, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:00:55.667515666Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:00:55.667519565Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.667523198Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:55.667527684Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:55.667556248Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(5), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:00:55.667586365Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }, filepath = "./demo/sftp/std/testing/out/./512B_random", offset = 0, len = 32768 +[2025-11-18T04:00:55.667608467Z WARN sunset_demo_sftp_std::demosftpserver] Read operation: length + offset > file length. Clipping ( 32768 + 0 > 512) +[2025-11-18T04:00:55.667633306Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 512 +[2025-11-18T04:00:55.667666531Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:00:55.667692091Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.667700600Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.667704438Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.667729555Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.713066511Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:00:55.713107103Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 6, 0, 0, 0, 4, 55, 15, 63, 111, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 126, 0] +[2025-11-18T04:00:55.713116827Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:00:55.713120696Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.713124369Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:55.713129041Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:55.713134649Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(6), Read { handle: FileHandle(BinString(len=4)), offset: 512, len: 32256 }) +[2025-11-18T04:00:55.713142644Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }, filepath = "./demo/sftp/std/testing/out/./512B_random", offset = 512, len = 32256 +[2025-11-18T04:00:55.713156751Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF +[2025-11-18T04:00:55.713163511Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.713189492Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.713196540Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.713200728Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.713610407Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:00:55.713647800Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 7, 0, 0, 0, 4, 55, 15, 63, 111] +[2025-11-18T04:00:55.713656525Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:00:55.713660343Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.713663954Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:00:55.713668225Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:00:55.713672875Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(7), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:00:55.713678710Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./512B_random" was successful +[2025-11-18T04:00:55.713691067Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.713694741Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.713698280Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.713701769Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.714137882Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:00:55.714168771Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 8, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:00:55.714181489Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:00:55.714185317Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.714188939Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:00:55.714193034Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.714216906Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(8), LStat { file_path: TextString("./demo/sftp/std/testing/out/./16kB_random") }) +[2025-11-18T04:00:55.714246807Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./16kB_random" +[2025-11-18T04:00:55.714286124Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./16kB_random" is Attrs { size: Some(16384), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438454), ext_count: None } +[2025-11-18T04:00:55.714319174Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.714326356Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.714349487Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.714374768Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.714840299Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:00:55.714870345Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 9, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:00:55.714881622Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:00:55.714885419Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.714889072Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:00:55.714893177Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.714918891Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(9), Stat { file_path: TextString("./demo/sftp/std/testing/out/./16kB_random") }) +[2025-11-18T04:00:55.714946879Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./16kB_random" +[2025-11-18T04:00:55.714985578Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./16kB_random" is Attrs { size: Some(16384), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438454), ext_count: None } +[2025-11-18T04:00:55.715021828Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.715028969Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.715032828Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.715036357Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.715466142Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:00:55.715495179Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 10, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:00:55.715506652Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:00:55.715510500Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.715514122Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:00:55.715518218Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:00:55.715543684Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(10), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./16kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:00:55.715573792Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./16kB_random", mode = SSH_FXF_READ +[2025-11-18T04:00:55.715601357Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:00:55.715645088Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./16kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }) +[2025-11-18T04:00:55.715675926Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.715683221Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.715706260Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.715731798Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.716266753Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:00:55.716297673Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 11, 0, 0, 0, 4, 204, 179, 134, 195, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:00:55.716307592Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:00:55.716311482Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.716315124Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:55.716338348Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:55.716366994Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(11), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:00:55.716379578Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }, filepath = "./demo/sftp/std/testing/out/./16kB_random", offset = 0, len = 32768 +[2025-11-18T04:00:55.716412001Z WARN sunset_demo_sftp_std::demosftpserver] Read operation: length + offset > file length. Clipping ( 32768 + 0 > 16384) +[2025-11-18T04:00:55.716438867Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 16384 +[2025-11-18T04:00:55.762253804Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:00:55.762324452Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.762334053Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.762339033Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.762343859Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.763463786Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:00:55.763502124Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 12, 0, 0, 0, 4, 204, 179, 134, 195, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 64, 0] +[2025-11-18T04:00:55.763512085Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:00:55.763515943Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.763519689Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:55.763524350Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:55.763551062Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(12), Read { handle: FileHandle(BinString(len=4)), offset: 16384, len: 16384 }) +[2025-11-18T04:00:55.763582548Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }, filepath = "./demo/sftp/std/testing/out/./16kB_random", offset = 16384, len = 16384 +[2025-11-18T04:00:55.763619096Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF +[2025-11-18T04:00:55.763630240Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.763652990Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.763678303Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.763705405Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.764155975Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:00:55.764185908Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 13, 0, 0, 0, 4, 204, 179, 134, 195] +[2025-11-18T04:00:55.764194664Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:00:55.764198554Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.764202165Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:00:55.764206333Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:00:55.764231398Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(13), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:00:55.764260054Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./16kB_random" was successful +[2025-11-18T04:00:55.764296274Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.764304793Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.764308498Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.764331608Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.764773803Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:00:55.764804712Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 14, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:00:55.764816370Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:00:55.764820229Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.764823841Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:00:55.764827926Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.764852549Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(14), LStat { file_path: TextString("./demo/sftp/std/testing/out/./64kB_random") }) +[2025-11-18T04:00:55.764883345Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./64kB_random" +[2025-11-18T04:00:55.764924143Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./64kB_random" is Attrs { size: Some(65536), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438455), ext_count: None } +[2025-11-18T04:00:55.764963604Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.764971959Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.764995049Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.765003425Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.765431183Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:00:55.765462833Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 15, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:00:55.765475027Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:00:55.765479616Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.765483248Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:00:55.765508077Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.765518377Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(15), Stat { file_path: TextString("./demo/sftp/std/testing/out/./64kB_random") }) +[2025-11-18T04:00:55.765524664Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./64kB_random" +[2025-11-18T04:00:55.765539511Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./64kB_random" is Attrs { size: Some(65536), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438455), ext_count: None } +[2025-11-18T04:00:55.765567746Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.765574887Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.765578529Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.765584693Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.766011422Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:00:55.766042939Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 16, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:00:55.766054566Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:00:55.766058425Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.766062047Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:00:55.766066132Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:00:55.766090518Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(16), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./64kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:00:55.766123105Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./64kB_random", mode = SSH_FXF_READ +[2025-11-18T04:00:55.766149220Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:00:55.766195904Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./64kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }) +[2025-11-18T04:00:55.766227102Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.766235097Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.766238842Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.766260810Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.766789201Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:00:55.766822477Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 17, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:00:55.766831871Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:00:55.766835771Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.766839445Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:55.766843509Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:55.766849148Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(17), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:00:55.766857575Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 0, len = 32768 +[2025-11-18T04:00:55.766872742Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:00:55.818563436Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:00:55.818607218Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.818615470Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.818619627Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.818623455Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.819563191Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 58 bytes +[2025-11-18T04:00:55.819598515Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 18, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 25, 5, 0, 0, 0, 19, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:00:55.819610441Z TRACE sunset_sftp::sftphandler::sftphandler] Received 58 bytes to process +[2025-11-18T04:00:55.819614320Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.819618065Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 58 +[2025-11-18T04:00:55.819622459Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 58 +[2025-11-18T04:00:55.819628046Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(18), Read { handle: FileHandle(BinString(len=4)), offset: 32768, len: 32768 }) +[2025-11-18T04:00:55.819634909Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 32768, len = 32768 +[2025-11-18T04:00:55.819649798Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:00:55.868682151Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:00:55.868726129Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 29 bytes +[2025-11-18T04:00:55.868734330Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 29 +[2025-11-18T04:00:55.868738569Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:55.868743220Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:55.868749167Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(19), Read { handle: FileHandle(BinString(len=4)), offset: 65536, len: 32768 }) +[2025-11-18T04:00:55.868756535Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 65536, len = 32768 +[2025-11-18T04:00:55.868770940Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF +[2025-11-18T04:00:55.868777412Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.868781096Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.868784635Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.868788329Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.869538429Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:00:55.869571994Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 20, 0, 0, 0, 4, 195, 17, 206, 102] +[2025-11-18T04:00:55.869581419Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:00:55.869585236Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.869588920Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:00:55.869593468Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:00:55.869598643Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(20), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:00:55.869605013Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./64kB_random" was successful +[2025-11-18T04:00:55.869617021Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.869620848Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.869624378Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.869627928Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.870109675Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:00:55.870147304Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 21, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:00:55.870158911Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:00:55.870162780Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.870166381Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:00:55.870170487Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.870174983Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(21), LStat { file_path: TextString("./demo/sftp/std/testing/out/./65kB_random") }) +[2025-11-18T04:00:55.870181579Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./65kB_random" +[2025-11-18T04:00:55.870202528Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./65kB_random" is Attrs { size: Some(66560), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438455), ext_count: None } +[2025-11-18T04:00:55.870216543Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.870220267Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.870223797Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.870227316Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.870631336Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes +[2025-11-18T04:00:55.870661073Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 22, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109] +[2025-11-18T04:00:55.870672957Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process +[2025-11-18T04:00:55.870718725Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.870723098Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 +[2025-11-18T04:00:55.870727224Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 +[2025-11-18T04:00:55.870752393Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(22), Stat { file_path: TextString("./demo/sftp/std/testing/out/./65kB_random") }) +[2025-11-18T04:00:55.870781152Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./65kB_random" +[2025-11-18T04:00:55.870823699Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./65kB_random" is Attrs { size: Some(66560), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438455), ext_count: None } +[2025-11-18T04:00:55.870854805Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.870861977Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.870884809Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.870909864Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.871287348Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes +[2025-11-18T04:00:55.871316292Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 23, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] +[2025-11-18T04:00:55.871327775Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process +[2025-11-18T04:00:55.871331624Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.871335235Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 +[2025-11-18T04:00:55.871358994Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 +[2025-11-18T04:00:55.871386025Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(23), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./65kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) +[2025-11-18T04:00:55.871401984Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./65kB_random", mode = SSH_FXF_READ +[2025-11-18T04:00:55.871407159Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false +[2025-11-18T04:00:55.871429652Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./65kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }) +[2025-11-18T04:00:55.871437802Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:55.871441475Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:55.871465748Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:55.871472755Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:55.872012803Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:00:55.872042880Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 24, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:00:55.872051955Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:00:55.872055834Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:55.872059467Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:55.872063572Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:55.872068357Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(24), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) +[2025-11-18T04:00:55.872093875Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 0, len = 32768 +[2025-11-18T04:00:55.872129796Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:00:56.048175463Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:00:56.048246934Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:56.048263223Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:56.048272051Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:56.048280180Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:56.128752382Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 58 bytes +[2025-11-18T04:00:56.128827475Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 25, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 25, 5, 0, 0, 0, 26, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 128, 0] +[2025-11-18T04:00:56.128947400Z TRACE sunset_sftp::sftphandler::sftphandler] Received 58 bytes to process +[2025-11-18T04:00:56.129007450Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:56.129023039Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 58 +[2025-11-18T04:00:56.129073612Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 58 +[2025-11-18T04:00:56.129134588Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(25), Read { handle: FileHandle(BinString(len=4)), offset: 32768, len: 32768 }) +[2025-11-18T04:00:56.129197118Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 32768, len = 32768 +[2025-11-18T04:00:56.129281842Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 +[2025-11-18T04:00:56.571741252Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:00:56.571788635Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 29 bytes +[2025-11-18T04:00:56.571796620Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 29 +[2025-11-18T04:00:56.571800725Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:56.571805592Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:56.571836934Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(26), Read { handle: FileHandle(BinString(len=4)), offset: 65536, len: 32768 }) +[2025-11-18T04:00:56.571867299Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 65536, len = 32768 +[2025-11-18T04:00:56.571887909Z WARN sunset_demo_sftp_std::demosftpserver] Read operation: length + offset > file length. Clipping ( 32768 + 65536 > 66560) +[2025-11-18T04:00:56.571912048Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 1024 +[2025-11-18T04:00:56.571973714Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data +[2025-11-18T04:00:56.571998913Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:56.572006229Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:56.572010056Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:56.572013740Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:56.612043057Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-18T04:00:56.612080511Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 27, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 1, 4, 0, 0, 0, 124, 0] +[2025-11-18T04:00:56.612090348Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process +[2025-11-18T04:00:56.612094268Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:56.612097901Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-18T04:00:56.612102737Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-18T04:00:56.612129695Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(27), Read { handle: FileHandle(BinString(len=4)), offset: 66560, len: 31744 }) +[2025-11-18T04:00:56.612159638Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 66560, len = 31744 +[2025-11-18T04:00:56.612179991Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF +[2025-11-18T04:00:56.612188727Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:56.612192482Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:56.612213977Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:56.612221015Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:56.612667861Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-18T04:00:56.612702804Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 28, 0, 0, 0, 4, 220, 94, 228, 181] +[2025-11-18T04:00:56.612711519Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process +[2025-11-18T04:00:56.612715368Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer +[2025-11-18T04:00:56.612719288Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-18T04:00:56.612723435Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-18T04:00:56.612750280Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(28), Close { handle: FileHandle(BinString(len=4)) }) +[2025-11-18T04:00:56.612779163Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./65kB_random" was successful +[2025-11-18T04:00:56.612796758Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes +[2025-11-18T04:00:56.612819138Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 +[2025-11-18T04:00:56.612828162Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) +[2025-11-18T04:00:56.612832453Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel +[2025-11-18T04:00:56.613342157Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 0 bytes +[2025-11-18T04:00:56.613381792Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [] +[2025-11-18T04:00:56.613390528Z DEBUG sunset_sftp::sftphandler::sftphandler] client disconnected +[2025-11-18T04:00:56.613394932Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing returned: Err(ClientDisconnected) +[2025-11-18T04:00:56.613402269Z WARN sunset_demo_sftp_std] sftp_loop finished: Err(ChannelEOF) +[2025-11-18T04:00:56.613409008Z WARN sunset_demo_common::server] Ended with error ChannelEOF +[2025-11-18T04:00:56.613464932Z INFO sunset_demo_common::server] Listening on TCP:22... \ No newline at end of file diff --git a/demo/sftp/std/testing/test_get.sh b/demo/sftp/std/testing/test_get.sh index e498fee9..fa2f4c03 100755 --- a/demo/sftp/std/testing/test_get.sh +++ b/demo/sftp/std/testing/test_get.sh @@ -1,6 +1,6 @@ #!/bin/bash -echo "Testing Stats..." +echo "Testing Multiple GETs..." # Set remote server details REMOTE_HOST="192.168.69.2" @@ -42,7 +42,7 @@ echo "Cleaning up original files..." rm -f -r ./*_random # Download the files -sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +sftp -vvvvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'get ./%s\n' "${FILES[@]}") bye diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index bbf2c3ec..22855b5b 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -503,6 +503,7 @@ where let processing_loop = async { loop { + trace!("SFTP: About to read bytes from SSH Channel"); let lr = chan_in.read(buffer_in).await?; debug!("SFTP <---- received: {:?} bytes", lr); From 87ae690cf7074395b7b7af36a9e8809db7e54a0b Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 19 Nov 2025 11:27:57 +1100 Subject: [PATCH 358/393] [skip ci] small changes to make visible SftpOutputPipe total bytes communicated There is no data lost in these communications --- demo/sftp/std/src/main.rs | 5 +++-- sftp/src/sftphandler/sftpoutputchannelhandler.rs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 4f65f975..1ba35e54 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -213,8 +213,10 @@ async fn main(spawner: Spawner) { .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) .filter_module( "sunset_sftp::sftphandler::sftpoutputchannelhandler", - log::LevelFilter::Info, + log::LevelFilter::Debug, ) + // .filter_module("sunset::channel", log::LevelFilter::Trace) + // .filter_module("sunset_async::async_sunset", log::LevelFilter::Trace) // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) // .filter_module("sunset_sftp::sftpserver", log::LevelFilter::Info) @@ -222,7 +224,6 @@ async fn main(spawner: Spawner) { // .filter_module("sunset::encrypt", log::LevelFilter::Info) // .filter_module("sunset::conn", log::LevelFilter::Info) // .filter_module("sunset::kex", log::LevelFilter::Info) - // .filter_module("sunset_async::async_sunset", log::LevelFilter::Info) // .filter_module("async_io", log::LevelFilter::Info) // .filter_module("polling", log::LevelFilter::Info) // .filter_module("embassy_net", log::LevelFilter::Info) diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 85d051f8..29b611fe 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -87,7 +87,7 @@ impl<'a, const N: usize> SftpOutputConsumer<'a, N> { _total = *lock; } - debug!("Output Consumer: Reads {rl} bytes. Total {_total}"); + debug!("Output Consumer: ---> Reads {rl} bytes. Total {_total}"); if rl > 0 { self.ssh_chan_out.write_all(&buf[..rl]).await?; debug!("Output Consumer: Written {:?} bytes ", &buf[..rl].len()); @@ -156,7 +156,7 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { _total = *lock; } - debug!("Output Producer: Sends {:?} bytes. Total {_total}", buf.len()); + debug!("Output Producer: <--- Sends {:?} bytes. Total {_total}", buf.len()); trace!("Output Producer: Sending buffer {:?}", buf); // writer.write_all(buf); // ??? error[E0596]: cannot borrow `*writer` as mutable, as it is behind a `&` reference From 580b71dd1083e26d4bb7abf9876ea7fe26317de0 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 19 Nov 2025 11:32:39 +1100 Subject: [PATCH 359/393] [skip ci] Removing outdated check in SftpHandler.process The fragmented packet handling will take care of that situation --- sftp/src/sftphandler/sftphandler.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 22855b5b..f56e9dae 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -128,11 +128,12 @@ where trace!("Received {:} bytes to process", buf.len()); - if !matches!(self.state, SftpHandleState::Fragmented(_)) - & buf.len().lt(&SFTP_MINIMUM_PACKET_LEN) - { - return Err(WireError::PacketWrong.into()); - } + // TODO: Should go out. fragmented packet handling should take care of it + // if !matches!(self.state, SftpHandleState::Fragmented(_)) + // & buf.len().lt(&SFTP_MINIMUM_PACKET_LEN) + // { + // return Err(WireError::PacketWrong.into()); + // } trace!("Entering loop to process the full received buffer"); while buf.len() > 0 { From 057ecd5d8325aef376ad6417ef98d385d14dbfde Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 19 Nov 2025 13:37:47 +1100 Subject: [PATCH 360/393] [skip ci] Removed nested enum in Sftp process FSM --- sftp/src/sftphandler/sftphandler.rs | 422 +++++++++++++--------------- 1 file changed, 189 insertions(+), 233 deletions(-) diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index f56e9dae..38de0b93 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -3,8 +3,8 @@ use super::PartialWriteRequestTracker; use crate::error::SftpError; use crate::handles::OpaqueFileHandle; use crate::proto::{ - self, InitVersionLowest, LStat, ReqId, SFTP_MINIMUM_PACKET_LEN, SFTP_VERSION, - SftpNum, SftpPacket, Stat, StatusCode, + self, InitVersionLowest, LStat, ReqId, SFTP_VERSION, SftpNum, SftpPacket, Stat, + StatusCode, }; use crate::requestholder::{RequestHolder, RequestHolderError}; use crate::server::{DirReply, ReadReply}; @@ -33,13 +33,8 @@ enum SftpHandleState { Initializing, /// The handle is ready to process requests Idle, - /// The request is fragmented and needs special handling - Fragmented(FragmentedRequestState), -} - -/// FSM subset to handle fragmented request as part of [`SftpHandleState`] -#[derive(Debug, PartialEq, Eq)] -enum FragmentedRequestState { + // /// The request is fragmented and needs special handling + // Fragmented(FragmentedRequestState), /// A request that is clipped needs to be process. It cannot be decoded /// and more bytes are needed ProcessingClippedRequest, @@ -128,13 +123,6 @@ where trace!("Received {:} bytes to process", buf.len()); - // TODO: Should go out. fragmented packet handling should take care of it - // if !matches!(self.state, SftpHandleState::Fragmented(_)) - // & buf.len().lt(&SFTP_MINIMUM_PACKET_LEN) - // { - // return Err(WireError::PacketWrong.into()); - // } - trace!("Entering loop to process the full received buffer"); while buf.len() > 0 { debug!( @@ -144,210 +132,202 @@ where ); match &self.state { - // There is a fragmented request in process of processing - SftpHandleState::Fragmented(fragment_case) => match fragment_case { - FragmentedRequestState::ProcessingClippedRequest => { - if let Err(e) = self - .incomplete_request_holder - .try_append_for_valid_request(&buf) - { - match e { - RequestHolderError::RanOut => { - warn!( - "There was not enough bytes in the buffer_in. \ + SftpHandleState::ProcessingClippedRequest => { + if let Err(e) = self + .incomplete_request_holder + .try_append_for_valid_request(&buf) + { + match e { + RequestHolderError::RanOut => { + warn!( + "There was not enough bytes in the buffer_in. \ We will continue adding bytes" - ); - buf = &buf[self - .incomplete_request_holder - .appended()..]; - continue; - } - RequestHolderError::WireError(WireError::RanOut) => { - warn!( - "WIRE ERROR: There was not enough bytes in the buffer_in. \ + ); + buf = &buf + [self.incomplete_request_holder.appended()..]; + continue; + } + RequestHolderError::WireError(WireError::RanOut) => { + warn!( + "WIRE ERROR: There was not enough bytes in the buffer_in. \ We will continue adding bytes" - ); - buf = &buf[self - .incomplete_request_holder - .appended()..]; - continue; - } - RequestHolderError::NoRoom => { - warn!( - "The request holder is full but the request in is incomplete. \ + ); + buf = &buf + [self.incomplete_request_holder.appended()..]; + continue; + } + RequestHolderError::NoRoom => { + warn!( + "The request holder is full but the request in is incomplete. \ We will try to decode it" - ); - } + ); + } - _ => { - error!( - "Unhandled error completing incomplete request {:?}", - e, - ); - return Err(SunsetError::Bug.into()); - } + _ => { + error!( + "Unhandled error completing incomplete request {:?}", + e, + ); + return Err(SunsetError::Bug.into()); } - } else { - debug!( - "Incomplete request holder completed the request!" - ); } + } else { + debug!("Incomplete request holder completed the request!"); + } - let used = self.incomplete_request_holder.appended(); - buf = &buf[used..]; + let used = self.incomplete_request_holder.appended(); + buf = &buf[used..]; - let mut source = SftpSource::new( - &self.incomplete_request_holder.try_get_ref()?, - ); - trace!("Internal Source Content: {:?}", source); + let mut source = SftpSource::new( + &self.incomplete_request_holder.try_get_ref()?, + ); + trace!("Internal Source Content: {:?}", source); - match SftpPacket::decode_request(&mut source) { - Ok(request) => { - Self::handle_general_request( - &mut self.file_server, - output_producer, - request, - ) - .await?; - self.incomplete_request_holder.reset(); - self.state = SftpHandleState::Idle; - } - Err(e) => match e { - WireError::UnknownPacket { number } => { - warn!( - "Unknown packet: packetId = {:?}. Will flush \ + match SftpPacket::decode_request(&mut source) { + Ok(request) => { + Self::handle_general_request( + &mut self.file_server, + output_producer, + request, + ) + .await?; + self.incomplete_request_holder.reset(); + self.state = SftpHandleState::Idle; + } + Err(e) => match e { + WireError::UnknownPacket { number } => { + warn!( + "Unknown packet: packetId = {:?}. Will flush \ its length and send unsupported back", - number - ); + number + ); - let req_id = ReqId(source.peak_packet_req_id()?); - let len = - source.peak_total_packet_len()? as usize; - source.consume_first(len)?; - output_producer - .send_status( - req_id, - StatusCode::SSH_FX_OP_UNSUPPORTED, - "Error decoding SFTP Packet", - ) - .await?; + let req_id = ReqId(source.peak_packet_req_id()?); + let len = source.peak_total_packet_len()? as usize; + source.consume_first(len)?; + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_OP_UNSUPPORTED, + "Error decoding SFTP Packet", + ) + .await?; + } + WireError::RanOut => match Self::handle_ran_out( + &mut self.file_server, + output_producer, + &mut source, + ) + .await + { + Ok(holder) => { + self.partial_write_request_tracker = + Some(holder); + self.incomplete_request_holder.reset(); + self.state = + SftpHandleState::ProcessingLongRequest; } - WireError::RanOut => match Self::handle_ran_out( - &mut self.file_server, - output_producer, - &mut source, - ) - .await - { - Ok(holder) => { - self.partial_write_request_tracker = - Some(holder); - self.incomplete_request_holder.reset(); - self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); + Err(e) => match e { + _ => { + error!( + "handle_ran_out finished with error: {:?}", + e + ); + return Err(SunsetError::Bug.into()); } - Err(e) => match e { - _ => { - error!( - "handle_ran_out finished with error: {:?}", - e - ); - return Err(SunsetError::Bug.into()); - } - }, }, - WireError::NoRoom => { - error!("Not enough space to fit the request") - } - _ => { - error!( - "Unhandled error decoding assembled packet: {:?}", - e - ); - return Err(WireError::PacketWrong.into()); - } }, - } + WireError::NoRoom => { + error!("Not enough space to fit the request") + } + _ => { + error!( + "Unhandled error decoding assembled packet: {:?}", + e + ); + return Err(WireError::PacketWrong.into()); + } + }, } - FragmentedRequestState::ProcessingLongRequest => { - let mut source = SftpSource::new(&buf); - trace!("Source content: {:?}", source); - - let mut write_tracker = if let Some(wt) = - self.partial_write_request_tracker.take() - { - wt - } else { - error!( - "BUG: FragmentedRequestState::ProcessingLongRequest cannot take the write tracker" - ); - return Err(SunsetError::Bug.into()); - }; + } + SftpHandleState::ProcessingLongRequest => { + let mut source = SftpSource::new(&buf); + trace!("Source content: {:?}", source); + + let mut write_tracker = if let Some(wt) = + self.partial_write_request_tracker.take() + { + wt + } else { + error!( + "BUG: SftpHandleState::ProcessingLongRequest cannot take the write tracker" + ); + return Err(SunsetError::Bug.into()); + }; - let opaque_handle = write_tracker.get_opaque_file_handle(); + let opaque_handle = write_tracker.get_opaque_file_handle(); - let usable_data = source - .remaining() - .min(write_tracker.get_remain_data_len() as usize); + let usable_data = source + .remaining() + .min(write_tracker.get_remain_data_len() as usize); - let data_segment = source.dec_as_binstring(usable_data)?; + let data_segment = source.dec_as_binstring(usable_data)?; - let data_segment_len = u32::try_from(data_segment.0.len()) - .map_err(|e| { + let data_segment_len = u32::try_from(data_segment.0.len()) + .map_err(|e| { error!("Error casting data segment len to u32: {e}"); SunsetError::Bug })?; - let current_write_offset = - write_tracker.get_remain_data_offset(); - write_tracker - .update_remaining_after_partial_write(data_segment_len); + let current_write_offset = + write_tracker.get_remain_data_offset(); + write_tracker + .update_remaining_after_partial_write(data_segment_len); - debug!( - "Processing successive chunks of a long write packet. \ + debug!( + "Processing successive chunks of a long write packet. \ Writing : opaque_handle = {:?}, write_offset = {:?}, \ data_segment = {:?}, data remaining = {:?}", - opaque_handle, - current_write_offset, - data_segment, - write_tracker.get_remain_data_len() - ); - - match self.file_server.write( - &opaque_handle, - current_write_offset, - data_segment.as_ref(), - ) { - Ok(_) => { - if write_tracker.get_remain_data_len() > 0 { - self.partial_write_request_tracker = - Some(write_tracker); - } else { - output_producer - .send_status( - write_tracker.get_req_id(), - StatusCode::SSH_FX_OK, - "", - ) - .await?; - info!("Finished multi part Write Request"); - self.state = SftpHandleState::Idle; - } - } - Err(e) => { - error!("SFTP write thrown: {:?}", e); + opaque_handle, + current_write_offset, + data_segment, + write_tracker.get_remain_data_len() + ); + + match self.file_server.write( + &opaque_handle, + current_write_offset, + data_segment.as_ref(), + ) { + Ok(_) => { + if write_tracker.get_remain_data_len() > 0 { + self.partial_write_request_tracker = + Some(write_tracker); + } else { output_producer .send_status( write_tracker.get_req_id(), - StatusCode::SSH_FX_FAILURE, - "error writing", + StatusCode::SSH_FX_OK, + "", ) .await?; + info!("Finished multi part Write Request"); self.state = SftpHandleState::Idle; } - }; - buf = &buf[buf.len() - source.remaining()..]; - } - }, - + } + Err(e) => { + error!("SFTP write thrown: {:?}", e); + output_producer + .send_status( + write_tracker.get_req_id(), + StatusCode::SSH_FX_FAILURE, + "error writing", + ) + .await?; + self.state = SftpHandleState::Idle; + } + }; + buf = &buf[buf.len() - source.remaining()..]; + } SftpHandleState::Initializing => { let (source, sftp_packet) = create_sftp_source_and_packet(buf); match sftp_packet { @@ -410,7 +390,8 @@ where Ok(holder) => { self.partial_write_request_tracker = Some(holder); - self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingLongRequest); + self.state = + SftpHandleState::ProcessingLongRequest; } Err(e) => { error!("Error handle_ran_out"); @@ -423,7 +404,7 @@ where .try_hold(&buf)?; buf = &buf[read..]; - self.state = SftpHandleState::Fragmented(FragmentedRequestState::ProcessingClippedRequest); + self.state = SftpHandleState::ProcessingClippedRequest; continue; } _ => { @@ -439,19 +420,7 @@ where its length and send unsupported back", number ); - /* TODO: The packet is unknown, but we are not consuming it properly. - That has the side effect that we will be interpreting chunks of that packet - as new packets and sending more garbage back to the client - Will result on an Close and not simply a clean message saying that - - Peek out the ReqId - - Peek out the length - - flush the packet len if possible (do we have the whole length?) - - - Send an StatusCode with the relevant ReqId - - - Move on! - */ let req_id = ReqId(source.peak_packet_req_id()?); let len = source.peak_total_packet_len()? as usize; source.consume_first(len)?; @@ -485,13 +454,12 @@ where Ok(()) } - /// WIP: A loop that will process all the request from stdio until + /// Take the [`ChanInOut`] and locks, Processing all the request from stdio until /// an EOF is received pub async fn process_loop( &mut self, stdio: ChanInOut<'a>, buffer_in: &mut [u8], - // buffer_out: &'a mut [u8], ) -> SftpResult<()> { let (mut chan_in, chan_out) = stdio.split(); @@ -662,7 +630,7 @@ where }; } SftpPacket::Read(req_id, read) => { - match file_server + if let Err(error) = file_server .read( &T::try_from(&read.handle)?, read.offset, @@ -671,32 +639,19 @@ where ) .await { - Ok(()) => { - // debug!("List stats for {} is {:?}", path, attrs); - - // output_producer - // .send_packet(&SftpPacket::Attrs(req_id, attrs)) - // .await?; - } - Err(error) => { - error!("Error reading data: {:?}", error); - if let SftpError::FileServerError(status) = error { - output_producer - .send_status( - req_id, - status, - "Could not list attributes", - ) - .await?; - } else { - output_producer - .send_status( - req_id, - StatusCode::SSH_FX_FAILURE, - "Could not list attributes", - ) - .await?; - } + error!("Error reading data: {:?}", error); + if let SftpError::FileServerError(status) = error { + output_producer + .send_status(req_id, status, "Could not list attributes") + .await?; + } else { + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_FAILURE, + "Could not list attributes", + ) + .await?; } } } @@ -747,7 +702,7 @@ where /// will require to be handled differently. Gathering the data in and /// processing it as we receive it in the channel in buffer. /// - /// In the current approach a tracker is required to store the state of + /// In the current approach, a tracker is required to store the state of /// the processing of such long requests. /// /// With an implementation that where able to hold the channel_in there might @@ -824,8 +779,9 @@ where } } -/// Function to create an SFTP source and decode an SFTP packet from it -/// to avoid code duplication +/// Function to create an SFTP source and decode an SFTP packet from it. +/// +/// Defined to avoid code duplication. fn create_sftp_source_and_packet( buf: &[u8], ) -> (SftpSource<'_>, Result, WireError>) { From c73ab4c1e8875fdb0813125ee10005cfa45f9dbe Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 19 Nov 2025 15:08:36 +1100 Subject: [PATCH 361/393] [skip ci]some tiding up --- demo/sftp/std/src/demosftpserver.rs | 16 +++++--- sftp/src/requestholder.rs | 2 +- sftp/src/sftphandler/sftphandler.rs | 18 +++++---- sftp/src/sftpserver.rs | 61 ++++++++++++++++++++++------- sftp/src/sftpsource.rs | 2 +- 5 files changed, 69 insertions(+), 30 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index a78f8055..c44b34b2 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -17,6 +17,9 @@ use std::fs; use std::os::unix::fs::PermissionsExt; use std::{fs::File, os::unix::fs::FileExt, path::Path}; +// Used during read operations +const ARBITRARY_READ_BUFFER_LENGTH: usize = 1024; + #[derive(Debug)] pub(crate) enum PrivatePathHandle { File(PrivateFileHandle), @@ -258,7 +261,10 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { .len(); if offset >= file_len { - info!("offset is larger than file length, sending EOF"); + info!( + "offset is larger than file length, sending EOF for {:?}", + private_file_handle.path + ); reply.send_eof().await.map_err(|err| { error!("Could not sent EOF: {:?}", err); StatusCode::SSH_FX_FAILURE @@ -269,16 +275,14 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { let read_len = if file_len >= len as u64 + offset { len } else { - warn!("Read operation: length + offset > file length. Clipping ( {:?} + {:?} > {:?})", + debug!("Read operation: length + offset > file length. Clipping ( {:?} + {:?} > {:?})", len, offset, file_len); (file_len - offset).try_into().unwrap_or(u32::MAX) }; - reply.send_header(offset, read_len).await?; - - const ARBITRARY_BUFFER_LENGTH: usize = 1024; + reply.send_header(read_len).await?; - let mut read_buff = [0u8; ARBITRARY_BUFFER_LENGTH]; + let mut read_buff = [0u8; ARBITRARY_READ_BUFFER_LENGTH]; let mut running_offset = offset; let mut remaining = read_len as usize; diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs index 81dc9411..9d4a3781 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/requestholder.rs @@ -167,7 +167,7 @@ impl<'a> RequestHolder<'a> { .unwrap_or(0); if complete_to_id_index > 0 { - warn!( + debug!( "The held fragment len = {:?}, is insufficient to peak \ the length and type. Will append {:?} to reach the \ id field index: {:?}", diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 38de0b93..c271bde8 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -139,7 +139,7 @@ where { match e { RequestHolderError::RanOut => { - warn!( + debug!( "There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); @@ -148,7 +148,7 @@ where continue; } RequestHolderError::WireError(WireError::RanOut) => { - warn!( + debug!( "WIRE ERROR: There was not enough bytes in the buffer_in. \ We will continue adding bytes" ); @@ -157,7 +157,7 @@ where continue; } RequestHolderError::NoRoom => { - warn!( + debug!( "The request holder is full but the request in is incomplete. \ We will try to decode it" ); @@ -310,7 +310,7 @@ where "", ) .await?; - info!("Finished multi part Write Request"); + debug!("Finished multi part Write Request"); self.state = SftpHandleState::Idle; } } @@ -375,8 +375,8 @@ where } Err(e) => match e { WireError::RanOut => { - warn!( - "RanOut for the SFTP Packet in the source buffer: {:?}", + debug!( + "The packet cannot fit in the receiving buffer: It will be handled in parts ({:?})", e ); @@ -394,7 +394,7 @@ where SftpHandleState::ProcessingLongRequest; } Err(e) => { - error!("Error handle_ran_out"); + debug!("Error handle_ran_out : {:?}", e); match e { SftpError::WireError( WireError::RanOut, @@ -408,6 +408,10 @@ where continue; } _ => { + error!( + "Error handle_ran_out: No Could not be catch {:?}", + e + ); return Err(SunsetError::Bug.into()); } } diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index ab58ce75..af17b38a 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,7 +1,7 @@ use crate::error::{SftpError, SftpResult}; use crate::proto::{ ENCODED_BASE_DATA_SFTP_PACKET_LENGTH, ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, - MAX_NAME_ENTRY_SIZE, NameEntry, PFlags, + MAX_NAME_ENTRY_SIZE, NameEntry, PFlags, SftpNum, SftpPacket, }; use crate::server::SftpSink; use crate::sftphandler::SftpOutputProducer; @@ -155,10 +155,21 @@ where } // TODO Define this -/// **This is a work in progress** /// A reference structure passed to the [`SftpServer::read()`] method to /// allow replying with the read data. - +/// Uses for [`ReadReply`] to: +/// +/// - In case of no more data avaliable to be sent, call `reply.send_eof()` +/// - There is data to be sent from an open file: +/// 1. Call `reply.send_header()` with the length of data to be sent +/// 2. Call `reply.send_data()` as many times as needed to complete a +/// sent of data of the announced length +/// 3. Do not call `reply.send_eof()` during this [`read`] method call +/// +/// It handles immutable sending data via the underlying sftp-channel +/// [`sunset_async::async_channel::ChanOut`] used in the context of an +/// SFTP Session. +/// pub struct ReadReply<'g, const N: usize> { /// The request Id that will be use`d in the response req_id: ReqId, @@ -178,11 +189,15 @@ impl<'g, const N: usize> ReadReply<'g, N> { } // TODO Make this enforceable - // TODO Document - pub async fn send_header(&self, offset: u64, data_len: u32) -> SftpResult<()> { + /// Sends a header fro `SSH_FXP_DATA` response. This includes the total + /// response length, the packet type, request id and data length + /// + /// The packet data content, excluding the length must be sent using + /// [`ReadReply::send_data`] + pub async fn send_header(&self, data_len: u32) -> SftpResult<()> { debug!( - "ReadReply: Sending header for request id {:?}: offset = {:?}, data length = {:?}", - self.req_id, offset, data_len + "ReadReply: Sending header for request id {:?}: data length = {:?}", + self.req_id, data_len ); let mut s = [0u8; N]; let mut sink = SftpSink::new(&mut s); @@ -190,7 +205,7 @@ impl<'g, const N: usize> ReadReply<'g, N> { // Encoding length field (data_len + ENCODED_BASE_DATA_SFTP_PACKET_LENGTH).enc(&mut sink)?; // Encoding packet type - 103u8.enc(&mut sink)?; // TODO Replace hack with + u8::from(SftpNum::SSH_FXP_DATA).enc(&mut sink)?; // Encoding req_id self.req_id.enc(&mut sink)?; // string data length @@ -207,17 +222,30 @@ impl<'g, const N: usize> ReadReply<'g, N> { Ok(()) } - /// Sends a buffer with data + /// Sends a buffer with data. Call it as many times as needed to send + /// the announced data length /// - /// Call this + /// **Important**: Call this after you have called `send_header` pub async fn send_data(&self, buff: &[u8]) -> SftpResult<()> { self.chan_out.send_data(buff).await } - /// Sends EOF meaning that there is no more files in the directory + /// Sends EOF meaning that there is no more data to be sent + /// pub async fn send_eof(&self) -> SftpResult<()> { self.chan_out.send_status(self.req_id, StatusCode::SSH_FX_EOF, "").await } + + /// Sends EOF with request id increased by one + /// + /// **Warning**: This is an experimental patch to resolve the situations where a response gets + /// stuck + /// + pub async fn send_eof_plus_one(&self) -> SftpResult<()> { + self.chan_out + .send_status(ReqId(self.req_id.0 + 1), StatusCode::SSH_FX_EOF, "") + .await + } } /// Uses for [`DirReply`] to: @@ -273,7 +301,7 @@ impl<'g, const N: usize> DirReply<'g, N> { // This way I collect data required for the header and collect // valid entries into a vector (only std) (items_encoded_len + ENCODED_BASE_NAME_SFTP_PACKET_LENGTH).enc(&mut sink)?; - 104u8.enc(&mut sink)?; // TODO Replace hack with + u8::from(SftpNum::SSH_FXP_NAME).enc(&mut sink)?; self.req_id.enc(&mut sink)?; count.enc(&mut sink)?; let payload = sink.payload_slice(); @@ -315,7 +343,11 @@ pub mod helpers { use sunset::sshwire::SSHEncode; - /// Helper function to get the length of a [`NameEntry`] + /// Helper function to get the length of a given [`NameEntry`] + /// as it would be serialized to the wire. + /// + /// Use this function to calculate the total length of a collection + /// of NameEntrys in order to send a correct response Name header pub fn get_name_entry_len(name_entry: &NameEntry<'_>) -> SftpResult { let mut buf = [0u8; MAX_NAME_ENTRY_SIZE]; let mut temp_sink = SftpSink::new(&mut buf); @@ -324,7 +356,6 @@ pub mod helpers { } } -// TODO Add this to SFTP library only available with std as a global helper #[cfg(feature = "std")] use crate::proto::Filename; #[cfg(feature = "std")] @@ -340,7 +371,7 @@ use std::{ /// /// WIP: Not stable. It has know issues and most likely it's methods will change /// -/// BUG: It does not include longname and that may be an issue +/// TODO: It does not include longname and that may be an issue #[derive(Debug)] pub struct DirEntriesCollection { /// Number of elements diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 0266dbd6..8e70c589 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -61,7 +61,7 @@ impl<'de> SftpSource<'de> { /// the result will contains garbage pub(crate) fn peak_packet_type(&self) -> WireResult { if self.buffer.len() <= SFTP_FIELD_ID_INDEX { - error!( + debug!( "Peak packet type failed: buffer len <= SFTP_FIELD_ID_INDEX ( {:?} <= {:?})", self.buffer.len(), SFTP_FIELD_ID_INDEX From fdc76ae08c6733dccfa5a1338cc644d7642c8d74 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 19 Nov 2025 15:08:59 +1100 Subject: [PATCH 362/393] adding large file 2MB to test_get.sh --- demo/sftp/std/testing/test_get.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/sftp/std/testing/test_get.sh b/demo/sftp/std/testing/test_get.sh index fa2f4c03..b46d91c4 100755 --- a/demo/sftp/std/testing/test_get.sh +++ b/demo/sftp/std/testing/test_get.sh @@ -7,7 +7,7 @@ REMOTE_HOST="192.168.69.2" REMOTE_USER="any" # Define test files -FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random") +FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random" "2048kB_random") # Generate random data files echo "Generating random data files..." From 5bad3fcf2223ec303430cfa4717788ce4857bbe6 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 19 Nov 2025 15:16:13 +1100 Subject: [PATCH 363/393] [skip ci] Bug? Large write buffers cause for Chan_out cause ssh.server.progress fail: NoRoom Running `sunset_demo_sftp_std` and calling test_get.sh triggers this error from time to time --- demo/sftp/std/src/main.rs | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 1ba35e54..46668265 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -92,7 +92,14 @@ impl DemoServer for StdDemo { let ssh_loop_inner = async { loop { let mut ph = ProgressHolder::new(); - let ev = serv.progress(&mut ph).await?; + let ev = match serv.progress(&mut ph).await { + Ok(event) => event, + Err(e) => { + error!("server progress failed: {:?}", e); // NoRoom: 2048 Bytes Output buffer + return Err(e); + } + }; + trace!("ev {ev:?}"); match ev { ServEvent::SessionShell(a) => { @@ -148,7 +155,7 @@ impl DemoServer for StdDemo { // TODO Do some research to find reasonable default buffer lengths let mut buffer_in = [0u8; 512]; - let mut incomplete_request_buffer = [0u8; 256]; + let mut incomplete_request_buffer = [0u8; 512]; match { let stdio = serv.stdio(ch).await?; @@ -156,7 +163,7 @@ impl DemoServer for StdDemo { "./demo/sftp/std/testing/out/".to_string(), ); - SftpHandler::::new( + SftpHandler::::new( &mut file_server, &mut incomplete_request_buffer, ) @@ -206,15 +213,17 @@ async fn listen( async fn main(spawner: Spawner) { env_logger::builder() .filter_level(log::LevelFilter::Info) - .filter_module( - "sunset_demo_sftp_std::demosftpserver", - log::LevelFilter::Debug, - ) - .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) - .filter_module( - "sunset_sftp::sftphandler::sftpoutputchannelhandler", - log::LevelFilter::Debug, - ) + // .filter_module( + // "sunset_demo_sftp_std::demosftpserver", + // log::LevelFilter::Debug, + // ) + // .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) + // .filter_module("sunset_sftp", log::LevelFilter::Trace) + // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Debug) + // .filter_module( + // "sunset_sftp::sftphandler::sftpoutputchannelhandler", + // log::LevelFilter::Debug, + // ) // .filter_module("sunset::channel", log::LevelFilter::Trace) // .filter_module("sunset_async::async_sunset", log::LevelFilter::Trace) // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) From c28b72ae3d3a52047d216627980940c3f0e44d08 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 19 Nov 2025 16:51:47 +1100 Subject: [PATCH 364/393] [skip ci] Debugging runner.read_channel and .input Still haven't found where the missing requests are. If I am sending bad data, I am still expecting to receive the data already sent according to the SFTP client New logs can be found running sftp_std demo and ./test_get.sh --- demo/sftp/std/src/main.rs | 22 ++++++++++++---------- src/runner.rs | 12 +++++++++++- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 46668265..5e8f9eb5 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -163,7 +163,7 @@ impl DemoServer for StdDemo { "./demo/sftp/std/testing/out/".to_string(), ); - SftpHandler::::new( + SftpHandler::::new( &mut file_server, &mut incomplete_request_buffer, ) @@ -213,19 +213,21 @@ async fn listen( async fn main(spawner: Spawner) { env_logger::builder() .filter_level(log::LevelFilter::Info) - // .filter_module( - // "sunset_demo_sftp_std::demosftpserver", - // log::LevelFilter::Debug, - // ) - // .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) + .filter_module( + "sunset_demo_sftp_std::demosftpserver", + log::LevelFilter::Debug, + ) + .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) // .filter_module("sunset_sftp", log::LevelFilter::Trace) // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Debug) - // .filter_module( - // "sunset_sftp::sftphandler::sftpoutputchannelhandler", - // log::LevelFilter::Debug, - // ) + .filter_module( + "sunset_sftp::sftphandler::sftpoutputchannelhandler", + log::LevelFilter::Info, + ) // .filter_module("sunset::channel", log::LevelFilter::Trace) // .filter_module("sunset_async::async_sunset", log::LevelFilter::Trace) + .filter_module("sunset::runner", log::LevelFilter::Debug) + // .filter_module("sunset::traffic", log::LevelFilter::Trace) // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) // .filter_module("sunset_sftp::sftpserver", log::LevelFilter::Info) diff --git a/src/runner.rs b/src/runner.rs index fb65c2d0..59867925 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -392,7 +392,13 @@ impl<'a, CS: CliServ> Runner<'a, CS> { if !self.is_input_ready() { return Ok(0); } - self.traf_in.input(&mut self.keys, &mut self.conn.remote_version, buf) + + self.traf_in + .input(&mut self.keys, &mut self.conn.remote_version, buf) + .inspect(|value| { + // TODO Jubeor. Remove this inspection + debug!("wire input bytes ({:?}): {:?}", *value, &buf[..*value]) + }) } // Whether [`input()`](input) is ready @@ -530,6 +536,10 @@ impl<'a, CS: CliServ> Runner<'a, CS> { } let (len, complete) = self.traf_in.read_channel(chan.0, dt, buf); + // TODO Jubeor. Remove this if block + if len != 0 { + debug!("read_channel buff ({len:?}): {:?}", &buf[..len]); + } if let Some(x) = complete { self.finished_read_channel(chan, x)?; } From a6db09259513fdbbee8d997a08ad84f71dd30bfc Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 20 Nov 2025 15:42:37 +1100 Subject: [PATCH 365/393] [skip ci] Removed debug points from runner --- src/runner.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/runner.rs b/src/runner.rs index 59867925..caf22ea9 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -393,12 +393,7 @@ impl<'a, CS: CliServ> Runner<'a, CS> { return Ok(0); } - self.traf_in - .input(&mut self.keys, &mut self.conn.remote_version, buf) - .inspect(|value| { - // TODO Jubeor. Remove this inspection - debug!("wire input bytes ({:?}): {:?}", *value, &buf[..*value]) - }) + self.traf_in.input(&mut self.keys, &mut self.conn.remote_version, buf) } // Whether [`input()`](input) is ready @@ -536,10 +531,7 @@ impl<'a, CS: CliServ> Runner<'a, CS> { } let (len, complete) = self.traf_in.read_channel(chan.0, dt, buf); - // TODO Jubeor. Remove this if block - if len != 0 { - debug!("read_channel buff ({len:?}): {:?}", &buf[..len]); - } + if let Some(x) = complete { self.finished_read_channel(chan, x)?; } From 13b1c4adfa27b1d29f124b25ef2177fed389f89c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 20 Nov 2025 16:04:06 +1100 Subject: [PATCH 366/393] [skip ci] NoRoom Error mitigated. Found a bug handling RanOut in sftphandler The mitigation handles NoRoom also in chan_in read and tries reading again The bug happen handling RanOut occurring naturally in the chan_in buffer, requiring that I take a look on how to better deal with them. At the moment it exited the sftp_loop closing the connection --- demo/sftp/std/src/main.rs | 30 ++++++++++++++++++++--------- sftp/src/sftphandler/sftphandler.rs | 15 ++++++++++++--- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 5e8f9eb5..6828c7d1 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -95,8 +95,16 @@ impl DemoServer for StdDemo { let ev = match serv.progress(&mut ph).await { Ok(event) => event, Err(e) => { - error!("server progress failed: {:?}", e); // NoRoom: 2048 Bytes Output buffer - return Err(e); + match e { + Error::NoRoom {} => { + warn!("NoRoom triggered. Trying again"); + continue; + } + _ => { + error!("server progress failed: {:?}", e); // NoRoom: 2048 Bytes Output buffer + return Err(e); + } + } } }; @@ -214,19 +222,23 @@ async fn main(spawner: Spawner) { env_logger::builder() .filter_level(log::LevelFilter::Info) .filter_module( - "sunset_demo_sftp_std::demosftpserver", + "sunset_sftp::sftphandler::sftphandler", log::LevelFilter::Debug, ) - .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) + // .filter_module( + // "sunset_demo_sftp_std::demosftpserver", + // log::LevelFilter::Debug, + // ) + // .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) // .filter_module("sunset_sftp", log::LevelFilter::Trace) // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Debug) - .filter_module( - "sunset_sftp::sftphandler::sftpoutputchannelhandler", - log::LevelFilter::Info, - ) + // .filter_module( + // "sunset_sftp::sftphandler::sftpoutputchannelhandler", + // log::LevelFilter::Info, + // ) // .filter_module("sunset::channel", log::LevelFilter::Trace) // .filter_module("sunset_async::async_sunset", log::LevelFilter::Trace) - .filter_module("sunset::runner", log::LevelFilter::Debug) + // .filter_module("sunset::runner", log::LevelFilter::Debug) // .filter_module("sunset::traffic", log::LevelFilter::Trace) // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index c271bde8..ad52988b 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -477,7 +477,16 @@ where let processing_loop = async { loop { trace!("SFTP: About to read bytes from SSH Channel"); - let lr = chan_in.read(buffer_in).await?; + let lr: usize = match chan_in.read(buffer_in).await { + Ok(lr) => lr, + Err(e) => match e { + SunsetError::NoRoom {} => { + error!("SSH channel is full"); + continue; + } + _ => return Err(e.into()), + }, + }; debug!("SFTP <---- received: {:?} bytes", lr); trace!("SFTP <---- received: {:?}", &buffer_in[0..lr]); @@ -493,11 +502,11 @@ where }; match select(processing_loop, output_consumer_loop).await { embassy_futures::select::Either::First(r) => { - debug!("Processing returned: {:?}", r); + error!("Processing returned: {:?}", r); r } embassy_futures::select::Either::Second(r) => { - warn!("Output consumer returned: {:?}", r); + error!("Output consumer returned: {:?}", r); r } } From 1d1b48645cd74bbdbce272617af35da6dc1b9c8f Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 21 Nov 2025 09:56:21 +1100 Subject: [PATCH 367/393] [skip ci] RequestHolder function visibility reduced and capacity() added I added capacity for uses when I want to check if a given slice fits in the holder --- sftp/src/requestholder.rs | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/sftp/src/requestholder.rs b/sftp/src/requestholder.rs index 9d4a3781..a33a8fcf 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/requestholder.rs @@ -1,4 +1,7 @@ -use crate::{proto, sftpsource::SftpSource}; +use crate::{ + proto::{self, SFTP_FIELD_LEN_LENGTH}, + sftpsource::SftpSource, +}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; @@ -49,7 +52,7 @@ pub(crate) type RequestHolderResult = Result; /// - `reset`: reset counters and flags to allow `try_hold` a new request /// #[derive(Debug)] -pub struct RequestHolder<'a> { +pub(crate) struct RequestHolder<'a> { /// The buffer used to contain the data for the request buffer: &'a mut [u8], /// The index of the last byte in the buffer containing usable data @@ -63,7 +66,7 @@ pub struct RequestHolder<'a> { impl<'a> RequestHolder<'a> { /// The buffer will be used to hold a full request. Choose a /// reasonable size for this buffer. - pub fn new(buffer: &'a mut [u8]) -> Self { + pub(crate) fn new(buffer: &'a mut [u8]) -> Self { RequestHolder { buffer: buffer, buffer_fill_index: 0, @@ -72,6 +75,11 @@ impl<'a> RequestHolder<'a> { } } + /// Returns the maximum request size that the holder can hold. + pub(crate) fn capacity(&self) -> usize { + self.buffer.len() - SFTP_FIELD_LEN_LENGTH + } + /// Uses the internal buffer to store a copy of the provided slice /// /// The definition of `try_hold` and `try_append_slice` separately @@ -84,7 +92,7 @@ impl<'a> RequestHolder<'a> { /// - Ok(usize): the number of bytes read from the slice /// /// - `Err(Busy)`: If there has been a call to `try_hold` without a call to `reset` - pub fn try_hold(&mut self, slice: &[u8]) -> RequestHolderResult { + pub(crate) fn try_hold(&mut self, slice: &[u8]) -> RequestHolderResult { if self.busy { return Err(RequestHolderError::Busy); } @@ -101,7 +109,7 @@ impl<'a> RequestHolder<'a> { /// Resets the `appended()` counter. /// /// Will not clear the previous data from the buffer. - pub fn reset(&mut self) -> () { + pub(crate) fn reset(&mut self) -> () { self.busy = false; self.buffer_fill_index = 0; self.appended = 0; @@ -124,7 +132,7 @@ impl<'a> RequestHolder<'a> { /// - `Err(Empty)`: If the structure has not been loaded with `try_hold` /// /// - `Err(Bug)`: An unexpected condition arises - pub fn try_append_for_valid_request( + pub(crate) fn try_append_for_valid_request( &mut self, slice_in: &[u8], ) -> RequestHolderResult<()> { @@ -252,7 +260,7 @@ impl<'a> RequestHolder<'a> { } /// Gets a reference to the slice that it is holding - pub fn try_get_ref(&self) -> RequestHolderResult<&[u8]> { + pub(crate) fn try_get_ref(&self) -> RequestHolderResult<&[u8]> { if self.busy { debug!( "Returning reference to: {:?}", @@ -264,18 +272,18 @@ impl<'a> RequestHolder<'a> { } } - pub fn is_full(&mut self) -> bool { + pub(crate) fn is_full(&mut self) -> bool { self.buffer_fill_index == self.buffer.len() } #[allow(unused)] /// Returns true if it has a slice in its buffer - pub fn is_busy(&self) -> bool { + pub(crate) fn is_busy(&self) -> bool { self.busy } /// Returns the bytes appened in the last call to `try_append_for_valid_request` - pub fn appended(&self) -> usize { + pub(crate) fn appended(&self) -> usize { self.appended } From 10b006b2a3e83c2090951655984ccc35354c3feb Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 21 Nov 2025 09:56:47 +1100 Subject: [PATCH 368/393] [skip ci] Removing outdated logs --- .../std/testing/out/failing-get-client.log | 294 ----------- .../std/testing/out/failing-get-server.log | 471 ------------------ .../out/get-deadlock-client-killed.log | 55 -- .../std/testing/out/success-get-client.log | 308 ------------ .../std/testing/out/success-get-server.log | 351 ------------- 5 files changed, 1479 deletions(-) delete mode 100644 demo/sftp/std/testing/out/failing-get-client.log delete mode 100644 demo/sftp/std/testing/out/failing-get-server.log delete mode 100644 demo/sftp/std/testing/out/get-deadlock-client-killed.log delete mode 100644 demo/sftp/std/testing/out/success-get-client.log delete mode 100644 demo/sftp/std/testing/out/success-get-server.log diff --git a/demo/sftp/std/testing/out/failing-get-client.log b/demo/sftp/std/testing/out/failing-get-client.log deleted file mode 100644 index 3585756c..00000000 --- a/demo/sftp/std/testing/out/failing-get-client.log +++ /dev/null @@ -1,294 +0,0 @@ -Testing Multiple GETs... -Generating random data files... -Uploading files to any@192.168.69.2... -Connected to 192.168.69.2. -sftp> put ./512B_random -Uploading ./512B_random to ./demo/sftp/std/testing/out/512B_random -512B_random 100% 512 443.9KB/s 00:00 -sftp> put ./16kB_random -Uploading ./16kB_random to ./demo/sftp/std/testing/out/16kB_random -16kB_random 100% 16KB 260.0KB/s 00:00 -sftp> put ./64kB_random -Uploading ./64kB_random to ./demo/sftp/std/testing/out/64kB_random -64kB_random 100% 64KB 255.3KB/s 00:00 -sftp> put ./65kB_random -Uploading ./65kB_random to ./demo/sftp/std/testing/out/65kB_random -65kB_random 100% 65KB 201.3KB/s 00:00 -sftp> -sftp> bye -client_loop: send disconnect: Broken pipe -UPLOAD Test Results: -============= -Upload PASS: 512B_random -Upload PASS: 16kB_random -Upload PASS: 64kB_random -Upload PASS: 65kB_random -Cleaning up original files... -OpenSSH_8.9p1 Ubuntu-3ubuntu0.13, OpenSSL 3.0.2 15 Mar 2022 -debug1: Reading configuration data /home/jubeor/.ssh/config -debug1: Reading configuration data /etc/ssh/ssh_config -debug1: /etc/ssh/ssh_config line 19: include /etc/ssh/ssh_config.d/*.conf matched no files -debug1: /etc/ssh/ssh_config line 21: Applying options for * -debug2: resolve_canonicalize: hostname 192.168.69.2 is address -debug3: ssh_connect_direct: entering -debug1: Connecting to 192.168.69.2 [192.168.69.2] port 22. -debug3: set_sock_tos: set socket 3 IP_TOS 0x10 -debug1: Connection established. -debug1: identity file /home/jubeor/.ssh/id_rsa type 0 -debug1: identity file /home/jubeor/.ssh/id_rsa-cert type -1 -debug1: identity file /home/jubeor/.ssh/id_ecdsa type -1 -debug1: identity file /home/jubeor/.ssh/id_ecdsa-cert type -1 -debug1: identity file /home/jubeor/.ssh/id_ecdsa_sk type -1 -debug1: identity file /home/jubeor/.ssh/id_ecdsa_sk-cert type -1 -debug1: identity file /home/jubeor/.ssh/id_ed25519 type -1 -debug1: identity file /home/jubeor/.ssh/id_ed25519-cert type -1 -debug1: identity file /home/jubeor/.ssh/id_ed25519_sk type -1 -debug1: identity file /home/jubeor/.ssh/id_ed25519_sk-cert type -1 -debug1: identity file /home/jubeor/.ssh/id_xmss type -1 -debug1: identity file /home/jubeor/.ssh/id_xmss-cert type -1 -debug1: identity file /home/jubeor/.ssh/id_dsa type -1 -debug1: identity file /home/jubeor/.ssh/id_dsa-cert type -1 -debug1: Local version string SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.13 -debug1: Remote protocol version 2.0, remote software version Sunset-1 -debug1: compat_banner: no match: Sunset-1 -debug2: fd 3 setting O_NONBLOCK -debug1: Authenticating to 192.168.69.2:22 as 'any' -debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory -debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory -debug3: order_hostkeyalgs: no algorithms matched; accept original -debug3: send packet: type 20 -debug1: SSH2_MSG_KEXINIT sent -debug3: receive packet: type 20 -debug1: SSH2_MSG_KEXINIT received -debug2: local client KEXINIT proposal -debug2: KEX algorithms: curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,sntrup761x25519-sha512@openssh.com,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,ext-info-c,kex-strict-c-v00@openssh.com -debug2: host key algorithms: ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,sk-ecdsa-sha2-nistp256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256 -debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com -debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com -debug2: MACs ctos: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 -debug2: MACs stoc: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 -debug2: compression ctos: none,zlib@openssh.com,zlib -debug2: compression stoc: none,zlib@openssh.com,zlib -debug2: languages ctos: -debug2: languages stoc: -debug2: first_kex_follows 0 -debug2: reserved 0 -debug2: peer server KEXINIT proposal -debug2: KEX algorithms: mlkem768x25519-sha256,curve25519-sha256,curve25519-sha256@libssh.org,kex-strict-s-v00@openssh.com,kexguess2@matt.ucc.asn.au -debug2: host key algorithms: ssh-ed25519,rsa-sha2-256 -debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes256-ctr -debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes256-ctr -debug2: MACs ctos: hmac-sha2-256 -debug2: MACs stoc: hmac-sha2-256 -debug2: compression ctos: none -debug2: compression stoc: none -debug2: languages ctos: -debug2: languages stoc: -debug2: first_kex_follows 0 -debug2: reserved 0 -debug3: kex_choose_conf: will use strict KEX ordering -debug1: kex: algorithm: curve25519-sha256 -debug1: kex: host key algorithm: ssh-ed25519 -debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: compression: none -debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: compression: none -debug3: send packet: type 30 -debug1: expecting SSH2_MSG_KEX_ECDH_REPLY -debug3: receive packet: type 31 -debug1: SSH2_MSG_KEX_ECDH_REPLY received -debug1: Server host key: ssh-ed25519 SHA256:6zdZ/bJAJoPDsk3YaJeckvI8Vbl8JmpTVfJH1ldlhyQ -debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory -debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory -Warning: Permanently added '192.168.69.2' (ED25519) to the list of known hosts. -debug3: send packet: type 21 -debug1: ssh_packet_send2_wrapped: resetting send seqnr 3 -debug2: ssh_set_newkeys: mode 1 -debug1: rekey out after 134217728 blocks -debug1: SSH2_MSG_NEWKEYS sent -debug1: expecting SSH2_MSG_NEWKEYS -debug3: receive packet: type 21 -debug1: ssh_packet_read_poll2: resetting read seqnr 3 -debug1: SSH2_MSG_NEWKEYS received -debug2: ssh_set_newkeys: mode 0 -debug1: rekey in after 134217728 blocks -debug1: Will attempt key: /home/jubeor/.ssh/id_rsa RSA SHA256:Wc43s4t4VZ5DkRa7At3RMd9E0u/UH4CV75mWxI0G8A4 -debug1: Will attempt key: /home/jubeor/.ssh/id_ecdsa -debug1: Will attempt key: /home/jubeor/.ssh/id_ecdsa_sk -debug1: Will attempt key: /home/jubeor/.ssh/id_ed25519 -debug1: Will attempt key: /home/jubeor/.ssh/id_ed25519_sk -debug1: Will attempt key: /home/jubeor/.ssh/id_xmss -debug1: Will attempt key: /home/jubeor/.ssh/id_dsa -debug2: pubkey_prepare: done -debug3: send packet: type 5 -debug3: receive packet: type 7 -debug1: SSH2_MSG_EXT_INFO received -debug1: kex_input_ext_info: server-sig-algs= -debug3: receive packet: type 6 -debug2: service_accept: ssh-userauth -debug1: SSH2_MSG_SERVICE_ACCEPT received -debug3: send packet: type 50 -debug3: receive packet: type 52 -Authenticated to 192.168.69.2 ([192.168.69.2]:22) using "none". -debug1: channel 0: new [client-session] -debug3: ssh_session2_open: channel_new: 0 -debug2: channel 0: send open -debug3: send packet: type 90 -debug1: Entering interactive session. -debug1: pledge: network -debug3: receive packet: type 91 -debug2: channel_input_open_confirmation: channel 0: callback start -debug2: fd 3 setting TCP_NODELAY -debug3: set_sock_tos: set socket 3 IP_TOS 0x08 -debug2: client_session2_setup: id 0 -debug1: Sending environment. -debug3: Ignored env SHELL -debug3: Ignored env USER_ZDOTDIR -debug3: Ignored env COLORTERM -debug3: Ignored env WSL2_GUI_APPS_ENABLED -debug3: Ignored env TERM_PROGRAM_VERSION -debug3: Ignored env WSL_DISTRO_NAME -debug3: Ignored env NAME -debug3: Ignored env PWD -debug3: Ignored env NIX_PROFILES -debug3: Ignored env LOGNAME -debug3: Ignored env VSCODE_GIT_ASKPASS_NODE -debug3: Ignored env VSCODE_INJECTION -debug3: Ignored env HOME -debug1: channel 0: setting env LANG = "C.UTF-8" -debug2: channel 0: request env confirm 0 -debug3: send packet: type 98 -debug3: Ignored env WSL_INTEROP -debug3: Ignored env LS_COLORS -debug3: Ignored env WAYLAND_DISPLAY -debug3: Ignored env NIX_SSL_CERT_FILE -debug3: Ignored env GIT_ASKPASS -debug3: Ignored env VSCODE_GIT_ASKPASS_EXTRA_ARGS -debug3: Ignored env VSCODE_PYTHON_AUTOACTIVATE_GUARD -debug3: Ignored env TERM -debug3: Ignored env ZDOTDIR -debug3: Ignored env USER -debug3: Ignored env VSCODE_GIT_IPC_HANDLE -debug3: Ignored env DISPLAY -debug3: Ignored env SHLVL -debug3: Ignored env XDG_RUNTIME_DIR -debug3: Ignored env WSLENV -debug3: Ignored env VSCODE_GIT_ASKPASS_MAIN -debug3: Ignored env XDG_DATA_DIRS -debug3: Ignored env PATH -debug3: Ignored env DBUS_SESSION_BUS_ADDRESS -debug3: Ignored env HOSTTYPE -debug3: Ignored env PULSE_SERVER -debug3: Ignored env OLDPWD -debug3: Ignored env TERM_PROGRAM -debug3: Ignored env VSCODE_IPC_HOOK_CLI -debug3: Ignored env _ -debug1: Sending subsystem: sftp -debug2: channel 0: request subsystem confirm 1 -debug3: send packet: type 98 -debug2: channel_input_open_confirmation: channel 0: callback done -debug2: channel 0: open confirm rwindow 1000 rmax 1000 -debug3: receive packet: type 99 -debug2: channel_input_status_confirm: type 99 id 0 -debug2: subsystem request accepted on channel 0 -debug2: Remote version: 3 -Connected to 192.168.69.2. -debug2: Sending SSH2_FXP_REALPATH "." -debug3: Sent message fd 3 T:16 I:1 -debug3: SSH2_FXP_REALPATH . -> ./demo/sftp/std/testing/out/ -sftp> get ./512B_random -debug3: Looking up ./demo/sftp/std/testing/out/./512B_random -debug3: Sent message fd 3 T:7 I:2 -debug3: Received stat reply T:105 I:2 F:0x000f M:100644 -Fetching ./demo/sftp/std/testing/out/./512B_random to 512B_random -debug2: do_download: download remote "./demo/sftp/std/testing/out/./512B_random" to local "512B_random" -debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./512B_random" -debug3: Sent message fd 3 T:17 I:3 -debug3: Received stat reply T:105 I:3 F:0x000f M:100644 -debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./512B_random" -debug3: Sent remote message SSH2_FXP_OPEN I:4 P:./demo/sftp/std/testing/out/./512B_random M:0x0001 -512B_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) -debug3: Received reply T:103 I:5 R:1 -debug3: Received data 0 -> 511 -debug3: Short data block, re-requesting 512 -> 32767 ( 1) -debug3: Finish at 32768 ( 1) -debug3: Received reply T:101 I:6 R:1 -512B_random 100% 512 11.0KB/s 00:00 -debug3: Sent message SSH2_FXP_CLOSE I:7 -debug3: SSH2_FXP_STATUS 0 -sftp> get ./16kB_random -debug3: Looking up ./demo/sftp/std/testing/out/./16kB_random -debug3: Sent message fd 3 T:7 I:8 -debug3: Received stat reply T:105 I:8 F:0x000f M:100644 -Fetching ./demo/sftp/std/testing/out/./16kB_random to 16kB_random -debug2: do_download: download remote "./demo/sftp/std/testing/out/./16kB_random" to local "16kB_random" -debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./16kB_random" -debug3: Sent message fd 3 T:17 I:9 -debug2: channel 0: rcvd adjust 516 -debug3: Received stat reply T:105 I:9 F:0x000f M:100644 -debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./16kB_random" -debug3: Sent remote message SSH2_FXP_OPEN I:10 P:./demo/sftp/std/testing/out/./16kB_random M:0x0001 -16kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) -debug3: Received reply T:103 I:11 R:1 -debug3: Received data 0 -> 16383 -debug3: Short data block, re-requesting 16384 -> 32767 ( 1) -debug3: Finish at 32768 ( 1) -debug3: Received reply T:101 I:12 R:1 -16kB_random 100% 16KB 332.6KB/s 00:00 -debug3: Sent message SSH2_FXP_CLOSE I:13 -debug3: SSH2_FXP_STATUS 0 -sftp> get ./64kB_random -debug3: Looking up ./demo/sftp/std/testing/out/./64kB_random -debug3: Sent message fd 3 T:7 I:14 -debug3: Received stat reply T:105 I:14 F:0x000f M:100644 -Fetching ./demo/sftp/std/testing/out/./64kB_random to 64kB_random -debug2: do_download: download remote "./demo/sftp/std/testing/out/./64kB_random" to local "64kB_random" -debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./64kB_random" -debug3: Sent message fd 3 T:17 I:15 -debug3: Received stat reply T:105 I:15 F:0x000f M:100644 -debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./64kB_random" -debug3: Sent remote message SSH2_FXP_OPEN I:16 P:./demo/sftp/std/testing/out/./64kB_random M:0x0001 -64kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) -debug3: Received reply T:103 I:17 R:1 -debug3: Received data 0 -> 32767 -debug3: Request range 32768 -> 65535 (0/2) -debug3: Request range 65536 -> 98303 (1/2) -debug2: channel 0: rcvd adjust 520 -debug3: Received reply T:103 I:18 R:2 -debug3: Received data 32768 -> 65535 -debug3: Finish at 98304 ( 1) -debug3: Received reply T:101 I:19 R:1 -64kB_random 100% 64KB 631.8KB/s 00:00 -debug3: Sent message SSH2_FXP_CLOSE I:20 -debug3: SSH2_FXP_STATUS 0 -sftp> get ./65kB_random -debug3: Looking up ./demo/sftp/std/testing/out/./65kB_random -debug3: Sent message fd 3 T:7 I:21 -debug3: Received stat reply T:105 I:21 F:0x000f M:100644 -Fetching ./demo/sftp/std/testing/out/./65kB_random to 65kB_random -debug2: do_download: download remote "./demo/sftp/std/testing/out/./65kB_random" to local "65kB_random" -debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./65kB_random" -debug3: Sent message fd 3 T:17 I:22 -debug3: Received stat reply T:105 I:22 F:0x000f M:100644 -debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./65kB_random" -debug3: Sent remote message SSH2_FXP_OPEN I:23 P:./demo/sftp/std/testing/out/./65kB_random M:0x0001 -65kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) -debug2: channel 0: window 1998336 sent adjust 98816 -debug3: Received reply T:103 I:24 R:1 -debug3: Received data 0 -> 32767 -debug3: Request range 32768 -> 65535 (0/2) -debug3: Request range 65536 -> 98303 (1/2) -debug3: Received reply T:103 I:25 R:2 -debug3: Received data 32768 -> 65535 -debug3: Finish at 98304 ( 1) -65kB_random 98% 64KB 20.1KB/s - stalled -debug3: send packet: type 1 -debug1: channel 0: free: client-session, nchannels 1 -debug3: channel 0: status: The following connections are open: - #0 client-session (t4 r0 i0/0 o0/0 e[write]/0 fd 4/5/6 sock -1 cc -1 io 0x01/0x02) - -Killed by signal 15. -DOWNLOAD Test Results: -============= -Download PASS: 512B_random -Download PASS: 16kB_random -Download PASS: 64kB_random -Download FAIL: 65kB_random -Cleaning up local files... \ No newline at end of file diff --git a/demo/sftp/std/testing/out/failing-get-server.log b/demo/sftp/std/testing/out/failing-get-server.log deleted file mode 100644 index efcac68d..00000000 --- a/demo/sftp/std/testing/out/failing-get-server.log +++ /dev/null @@ -1,471 +0,0 @@ - 242, 216, 97, 231, 206, 41, 26, 80, 23, 166, 87, 216, 71, 123, 89, 97, 13, 199, 48, 33, 113, 221, 215, 37, 191, 200, 105, 224, 126, 243, 195, 183, 249, 7, 181, 145, 177, 255, 191, 135, 63, 24, 79, 236, 154, 162, 111, 118, 173, 93, 105, 233, 165, 81, 252, 183, 178, 8, 89, 191, 208, 158, 58, 150, 148, 175, 123, 117, 76, 252, 132, 135, 210, 65, 225, 245, 205, 211, 119, 7, 162, 181, 9, 224, 16, 97, 113, 255, 163, 84, 159, 139, 152, 113, 156, 196, 208, 178, 102, 173, 138, 4, 174, 6, 74, 210, 227, 38, 158, 122, 70, 62, 207, 185, 90, 30, 94, 48, 244, 145, 219, 154, 137, 163, 146, 154, 81, 17, 240, 14, 133, 160, 92, 248, 220, 243, 200, 97, 171, 56, 207, 137, 200, 51, 113, 22, 128, 241, 124, 167, 164, 18, 66, 253, 20, 120, 108, 25, 111, 86, 98, 98, 154, 185, 90, 211, 14, 129, 62, 167, 64, 58, 232, 195, 55, 137, 253, 147, 95, 45, 119, 228, 8, 220, 124, 7, 99, 56, 197, 234, 125, 126, 38, 4, 127, 208, 155, 203, 67, 197, 164, 116, 128, 106, 184, 41, 196, 192, 68, 13, 60, 13, 230, 212, 41, 210, 184, 19, 41, 88, 67, 141, 173, 227, 3, 34, 235, 144, 121, 169, 154, 4, 242, 223, 135, 158, 9, 100, 131, 238, 86, 166, 145, 180, 228, 134, 186, 136, 120, 121, 2, 229, 236, 171, 22, 1, 87, 205, 37, 121, 230, 77, 235, 100, 164, 205, 161, 12, 132, 179, 0, 246, 110, 117, 253, 104, 126, 163, 120, 161, 59, 192, 76, 157, 5, 49, 211, 147, 53], index: 0 } -[2025-11-18T04:07:30.367537929Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 63261, data_segment = BinString(len=512), data remaining = 1763 -[2025-11-18T04:07:30.367572804Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 63261, buffer length = 512, bytes written = 512 -[2025-11-18T04:07:30.367601266Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.367626578Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.367653249Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.367687599Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 296 bytes -[2025-11-18T04:07:30.367695505Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [119, 84, 132, 32, 133, 174, 0, 64, 218, 17, 73, 172, 110, 238, 156, 70, 127, 134, 71, 123, 38, 185, 224, 56, 114, 211, 128, 228, 225, 148, 170, 76, 132, 210, 119, 22, 27, 53, 253, 86, 156, 155, 85, 159, 225, 43, 147, 119, 153, 139, 40, 47, 81, 120, 2, 215, 220, 83, 255, 172, 85, 142, 3, 246, 159, 201, 214, 31, 73, 235, 27, 61, 194, 140, 67, 172, 63, 181, 2, 77, 73, 80, 223, 36, 211, 56, 118, 230, 42, 240, 247, 31, 246, 180, 85, 18, 139, 43, 212, 249, 186, 165, 123, 231, 63, 85, 187, 254, 117, 192, 165, 168, 50, 3, 237, 9, 71, 21, 152, 251, 27, 37, 123, 12, 252, 22, 58, 234, 165, 61, 196, 235, 84, 142, 243, 153, 116, 27, 141, 101, 66, 49, 107, 224, 222, 209, 225, 221, 34, 67, 114, 32, 216, 191, 120, 72, 61, 154, 61, 209, 194, 154, 230, 167, 33, 27, 233, 223, 155, 137, 137, 147, 246, 138, 1, 168, 103, 52, 84, 156, 65, 158, 146, 103, 43, 212, 248, 225, 111, 109, 4, 189, 0, 197, 1, 59, 160, 31, 220, 171, 113, 169, 68, 137, 55, 132, 249, 136, 198, 214, 151, 135, 212, 51, 142, 160, 46, 149, 105, 36, 194, 159, 110, 30, 42, 175, 29, 215, 148, 218, 252, 53, 182, 143, 11, 119, 159, 133, 124, 117, 141, 104, 63, 94, 156, 244, 152, 190, 137, 146, 67, 41, 50, 84, 161, 139, 230, 247, 234, 223, 223, 80, 172, 62, 163, 53, 81, 162, 190, 106, 89, 34, 238, 187, 133, 4, 131, 195, 163, 221, 27, 227, 231, 189, 8, 153, 142, 137, 118, 0, 144, 124, 101, 104, 14, 119] -[2025-11-18T04:07:30.367738605Z TRACE sunset_sftp::sftphandler::sftphandler] Received 296 bytes to process -[2025-11-18T04:07:30.367745676Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.367749403Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 296 -[2025-11-18T04:07:30.367773335Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [119, 84, 132, 32, 133, 174, 0, 64, 218, 17, 73, 172, 110, 238, 156, 70, 127, 134, 71, 123, 38, 185, 224, 56, 114, 211, 128, 228, 225, 148, 170, 76, 132, 210, 119, 22, 27, 53, 253, 86, 156, 155, 85, 159, 225, 43, 147, 119, 153, 139, 40, 47, 81, 120, 2, 215, 220, 83, 255, 172, 85, 142, 3, 246, 159, 201, 214, 31, 73, 235, 27, 61, 194, 140, 67, 172, 63, 181, 2, 77, 73, 80, 223, 36, 211, 56, 118, 230, 42, 240, 247, 31, 246, 180, 85, 18, 139, 43, 212, 249, 186, 165, 123, 231, 63, 85, 187, 254, 117, 192, 165, 168, 50, 3, 237, 9, 71, 21, 152, 251, 27, 37, 123, 12, 252, 22, 58, 234, 165, 61, 196, 235, 84, 142, 243, 153, 116, 27, 141, 101, 66, 49, 107, 224, 222, 209, 225, 221, 34, 67, 114, 32, 216, 191, 120, 72, 61, 154, 61, 209, 194, 154, 230, 167, 33, 27, 233, 223, 155, 137, 137, 147, 246, 138, 1, 168, 103, 52, 84, 156, 65, 158, 146, 103, 43, 212, 248, 225, 111, 109, 4, 189, 0, 197, 1, 59, 160, 31, 220, 171, 113, 169, 68, 137, 55, 132, 249, 136, 198, 214, 151, 135, 212, 51, 142, 160, 46, 149, 105, 36, 194, 159, 110, 30, 42, 175, 29, 215, 148, 218, 252, 53, 182, 143, 11, 119, 159, 133, 124, 117, 141, 104, 63, 94, 156, 244, 152, 190, 137, 146, 67, 41, 50, 84, 161, 139, 230, 247, 234, 223, 223, 80, 172, 62, 163, 53, 81, 162, 190, 106, 89, 34, 238, 187, 133, 4, 131, 195, 163, 221, 27, 227, 231, 189, 8, 153, 142, 137, 118, 0, 144, 124, 101, 104, 14, 119], index: 0 } -[2025-11-18T04:07:30.367815097Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 63773, data_segment = BinString(len=296), data remaining = 1467 -[2025-11-18T04:07:30.367850517Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 63773, buffer length = 296, bytes written = 296 -[2025-11-18T04:07:30.367880493Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.367887678Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.367891291Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.368029411Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 512 bytes -[2025-11-18T04:07:30.368058789Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [113, 234, 27, 32, 83, 239, 165, 127, 228, 129, 35, 216, 119, 156, 12, 52, 182, 135, 68, 235, 78, 105, 16, 151, 208, 75, 115, 123, 195, 209, 205, 173, 188, 123, 138, 41, 24, 230, 5, 226, 49, 210, 105, 38, 251, 40, 237, 78, 93, 10, 180, 33, 163, 36, 200, 142, 218, 88, 44, 24, 156, 107, 53, 39, 151, 180, 78, 240, 24, 169, 189, 142, 208, 186, 107, 0, 4, 52, 157, 138, 144, 177, 85, 164, 194, 57, 186, 52, 19, 84, 66, 243, 212, 220, 69, 223, 198, 87, 59, 166, 101, 17, 118, 177, 182, 164, 58, 141, 76, 98, 61, 2, 156, 97, 83, 189, 160, 233, 239, 235, 252, 68, 151, 106, 63, 18, 247, 139, 116, 13, 136, 63, 93, 42, 146, 111, 60, 73, 146, 112, 43, 199, 92, 166, 218, 147, 184, 60, 23, 38, 66, 104, 84, 222, 155, 98, 39, 81, 177, 170, 82, 147, 191, 212, 239, 58, 210, 141, 64, 101, 83, 24, 11, 71, 17, 100, 8, 232, 222, 37, 132, 218, 132, 204, 37, 89, 0, 208, 60, 205, 186, 143, 171, 76, 141, 123, 34, 26, 8, 199, 107, 57, 43, 194, 208, 225, 46, 125, 7, 45, 222, 43, 16, 112, 52, 109, 229, 28, 90, 143, 60, 181, 125, 24, 84, 119, 26, 36, 73, 12, 38, 13, 5, 163, 99, 45, 209, 100, 219, 228, 33, 204, 235, 109, 217, 209, 203, 39, 224, 121, 156, 154, 168, 78, 225, 57, 130, 148, 68, 85, 228, 76, 156, 184, 237, 37, 186, 230, 222, 133, 232, 192, 56, 20, 170, 130, 101, 0, 151, 225, 89, 183, 54, 253, 152, 178, 74, 226, 12, 8, 35, 240, 72, 49, 150, 212, 159, 233, 159, 57, 59, 27, 104, 248, 1, 169, 119, 179, 171, 128, 206, 94, 49, 155, 211, 200, 163, 185, 157, 196, 149, 161, 75, 198, 53, 98, 38, 190, 222, 250, 123, 16, 203, 175, 109, 8, 82, 43, 172, 30, 195, 126, 193, 14, 75, 214, 219, 208, 126, 166, 91, 151, 88, 88, 37, 195, 136, 59, 88, 122, 95, 236, 25, 145, 164, 136, 71, 51, 48, 86, 65, 226, 213, 19, 110, 233, 76, 81, 99, 255, 108, 157, 7, 240, 217, 219, 166, 50, 81, 93, 139, 183, 90, 76, 109, 144, 63, 158, 245, 22, 187, 221, 251, 220, 176, 195, 183, 207, 89, 79, 180, 121, 243, 111, 58, 179, 166, 171, 110, 62, 207, 167, 9, 183, 220, 49, 158, 232, 76, 15, 48, 249, 42, 247, 164, 209, 229, 90, 86, 38, 203, 145, 241, 40, 238, 17, 59, 47, 199, 146, 111, 200, 42, 207, 33, 106, 121, 248, 198, 162, 25, 52, 94, 34, 158, 218, 98, 185, 90, 222, 35, 131, 170, 193, 90, 202, 155, 38, 108, 84, 60, 166, 195, 124, 104, 104, 26, 78, 124, 141, 120, 86, 228, 214, 9, 250, 136, 225, 14, 23, 145, 67, 43, 39, 218, 43, 11, 170, 135, 85, 122, 108] -[2025-11-18T04:07:30.368118657Z TRACE sunset_sftp::sftphandler::sftphandler] Received 512 bytes to process -[2025-11-18T04:07:30.368126820Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.368132389Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 512 -[2025-11-18T04:07:30.368136826Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [113, 234, 27, 32, 83, 239, 165, 127, 228, 129, 35, 216, 119, 156, 12, 52, 182, 135, 68, 235, 78, 105, 16, 151, 208, 75, 115, 123, 195, 209, 205, 173, 188, 123, 138, 41, 24, 230, 5, 226, 49, 210, 105, 38, 251, 40, 237, 78, 93, 10, 180, 33, 163, 36, 200, 142, 218, 88, 44, 24, 156, 107, 53, 39, 151, 180, 78, 240, 24, 169, 189, 142, 208, 186, 107, 0, 4, 52, 157, 138, 144, 177, 85, 164, 194, 57, 186, 52, 19, 84, 66, 243, 212, 220, 69, 223, 198, 87, 59, 166, 101, 17, 118, 177, 182, 164, 58, 141, 76, 98, 61, 2, 156, 97, 83, 189, 160, 233, 239, 235, 252, 68, 151, 106, 63, 18, 247, 139, 116, 13, 136, 63, 93, 42, 146, 111, 60, 73, 146, 112, 43, 199, 92, 166, 218, 147, 184, 60, 23, 38, 66, 104, 84, 222, 155, 98, 39, 81, 177, 170, 82, 147, 191, 212, 239, 58, 210, 141, 64, 101, 83, 24, 11, 71, 17, 100, 8, 232, 222, 37, 132, 218, 132, 204, 37, 89, 0, 208, 60, 205, 186, 143, 171, 76, 141, 123, 34, 26, 8, 199, 107, 57, 43, 194, 208, 225, 46, 125, 7, 45, 222, 43, 16, 112, 52, 109, 229, 28, 90, 143, 60, 181, 125, 24, 84, 119, 26, 36, 73, 12, 38, 13, 5, 163, 99, 45, 209, 100, 219, 228, 33, 204, 235, 109, 217, 209, 203, 39, 224, 121, 156, 154, 168, 78, 225, 57, 130, 148, 68, 85, 228, 76, 156, 184, 237, 37, 186, 230, 222, 133, 232, 192, 56, 20, 170, 130, 101, 0, 151, 225, 89, 183, 54, 253, 152, 178, 74, 226, 12, 8, 35, 240, 72, 49, 150, 212, 159, 233, 159, 57, 59, 27, 104, 248, 1, 169, 119, 179, 171, 128, 206, 94, 49, 155, 211, 200, 163, 185, 157, 196, 149, 161, 75, 198, 53, 98, 38, 190, 222, 250, 123, 16, 203, 175, 109, 8, 82, 43, 172, 30, 195, 126, 193, 14, 75, 214, 219, 208, 126, 166, 91, 151, 88, 88, 37, 195, 136, 59, 88, 122, 95, 236, 25, 145, 164, 136, 71, 51, 48, 86, 65, 226, 213, 19, 110, 233, 76, 81, 99, 255, 108, 157, 7, 240, 217, 219, 166, 50, 81, 93, 139, 183, 90, 76, 109, 144, 63, 158, 245, 22, 187, 221, 251, 220, 176, 195, 183, 207, 89, 79, 180, 121, 243, 111, 58, 179, 166, 171, 110, 62, 207, 167, 9, 183, 220, 49, 158, 232, 76, 15, 48, 249, 42, 247, 164, 209, 229, 90, 86, 38, 203, 145, 241, 40, 238, 17, 59, 47, 199, 146, 111, 200, 42, 207, 33, 106, 121, 248, 198, 162, 25, 52, 94, 34, 158, 218, 98, 185, 90, 222, 35, 131, 170, 193, 90, 202, 155, 38, 108, 84, 60, 166, 195, 124, 104, 104, 26, 78, 124, 141, 120, 86, 228, 214, 9, 250, 136, 225, 14, 23, 145, 67, 43, 39, 218, 43, 11, 170, 135, 85, 122, 108], index: 0 } -[2025-11-18T04:07:30.368188552Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 64069, data_segment = BinString(len=512), data remaining = 955 -[2025-11-18T04:07:30.368239124Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 64069, buffer length = 512, bytes written = 512 -[2025-11-18T04:07:30.368273546Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.368280690Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.368308998Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.368326868Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 488 bytes -[2025-11-18T04:07:30.368351542Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [30, 162, 101, 210, 19, 161, 7, 70, 0, 249, 69, 185, 69, 174, 89, 60, 224, 5, 231, 15, 112, 230, 188, 13, 163, 229, 95, 109, 190, 203, 13, 27, 149, 186, 49, 213, 60, 21, 55, 117, 175, 176, 199, 127, 150, 103, 170, 98, 236, 108, 53, 0, 141, 149, 147, 93, 30, 186, 241, 62, 42, 179, 53, 182, 36, 238, 104, 247, 219, 14, 185, 174, 221, 254, 204, 253, 131, 239, 54, 168, 40, 199, 247, 66, 128, 198, 239, 181, 197, 147, 226, 6, 120, 94, 13, 254, 249, 34, 67, 37, 150, 162, 148, 142, 101, 255, 66, 49, 175, 41, 95, 2, 200, 122, 30, 225, 183, 169, 178, 245, 128, 88, 10, 100, 60, 24, 43, 206, 157, 96, 40, 148, 162, 205, 4, 188, 161, 220, 38, 223, 44, 198, 203, 128, 211, 105, 63, 123, 101, 112, 182, 242, 44, 61, 118, 77, 83, 169, 254, 246, 133, 204, 39, 72, 217, 137, 107, 146, 7, 247, 146, 91, 227, 66, 136, 217, 78, 0, 93, 65, 241, 147, 89, 28, 115, 188, 115, 16, 51, 174, 222, 174, 7, 207, 29, 196, 179, 165, 14, 73, 246, 22, 255, 207, 174, 142, 188, 254, 252, 52, 108, 126, 207, 235, 250, 54, 76, 11, 160, 131, 89, 51, 232, 136, 251, 233, 190, 177, 246, 250, 5, 153, 215, 16, 133, 94, 31, 76, 173, 115, 95, 75, 176, 165, 58, 235, 133, 181, 238, 47, 28, 60, 226, 101, 24, 173, 147, 104, 5, 227, 135, 76, 94, 106, 89, 4, 255, 221, 149, 10, 39, 206, 227, 219, 248, 118, 178, 227, 170, 98, 192, 119, 43, 181, 99, 4, 18, 120, 43, 247, 109, 147, 82, 119, 91, 52, 12, 98, 144, 170, 8, 33, 247, 97, 115, 76, 89, 177, 171, 51, 167, 206, 176, 199, 142, 104, 191, 248, 168, 202, 231, 87, 47, 63, 90, 123, 80, 82, 44, 249, 130, 173, 50, 78, 162, 252, 178, 58, 201, 104, 45, 132, 214, 141, 103, 128, 22, 149, 7, 186, 109, 250, 200, 53, 28, 157, 139, 158, 163, 235, 211, 242, 148, 110, 73, 230, 37, 193, 202, 56, 210, 44, 224, 44, 120, 199, 217, 248, 91, 224, 119, 213, 1, 30, 145, 5, 201, 170, 228, 231, 175, 137, 56, 64, 71, 126, 255, 195, 200, 0, 250, 25, 12, 82, 108, 136, 146, 163, 50, 142, 151, 120, 161, 201, 165, 164, 22, 223, 164, 194, 154, 165, 217, 75, 231, 79, 125, 233, 109, 155, 160, 245, 105, 121, 28, 199, 169, 155, 227, 222, 244, 79, 86, 49, 134, 30, 102, 194, 104, 238, 17, 210, 62, 225, 78, 104, 43, 139, 59, 83, 5, 9, 115, 149, 208, 135, 109, 189, 248, 227, 45, 51, 243, 135, 78, 236, 38, 99, 206, 132, 217, 195, 9, 227, 65, 137, 89, 116] -[2025-11-18T04:07:30.368409124Z TRACE sunset_sftp::sftphandler::sftphandler] Received 488 bytes to process -[2025-11-18T04:07:30.368416474Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.368420561Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 488 -[2025-11-18T04:07:30.368443330Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [30, 162, 101, 210, 19, 161, 7, 70, 0, 249, 69, 185, 69, 174, 89, 60, 224, 5, 231, 15, 112, 230, 188, 13, 163, 229, 95, 109, 190, 203, 13, 27, 149, 186, 49, 213, 60, 21, 55, 117, 175, 176, 199, 127, 150, 103, 170, 98, 236, 108, 53, 0, 141, 149, 147, 93, 30, 186, 241, 62, 42, 179, 53, 182, 36, 238, 104, 247, 219, 14, 185, 174, 221, 254, 204, 253, 131, 239, 54, 168, 40, 199, 247, 66, 128, 198, 239, 181, 197, 147, 226, 6, 120, 94, 13, 254, 249, 34, 67, 37, 150, 162, 148, 142, 101, 255, 66, 49, 175, 41, 95, 2, 200, 122, 30, 225, 183, 169, 178, 245, 128, 88, 10, 100, 60, 24, 43, 206, 157, 96, 40, 148, 162, 205, 4, 188, 161, 220, 38, 223, 44, 198, 203, 128, 211, 105, 63, 123, 101, 112, 182, 242, 44, 61, 118, 77, 83, 169, 254, 246, 133, 204, 39, 72, 217, 137, 107, 146, 7, 247, 146, 91, 227, 66, 136, 217, 78, 0, 93, 65, 241, 147, 89, 28, 115, 188, 115, 16, 51, 174, 222, 174, 7, 207, 29, 196, 179, 165, 14, 73, 246, 22, 255, 207, 174, 142, 188, 254, 252, 52, 108, 126, 207, 235, 250, 54, 76, 11, 160, 131, 89, 51, 232, 136, 251, 233, 190, 177, 246, 250, 5, 153, 215, 16, 133, 94, 31, 76, 173, 115, 95, 75, 176, 165, 58, 235, 133, 181, 238, 47, 28, 60, 226, 101, 24, 173, 147, 104, 5, 227, 135, 76, 94, 106, 89, 4, 255, 221, 149, 10, 39, 206, 227, 219, 248, 118, 178, 227, 170, 98, 192, 119, 43, 181, 99, 4, 18, 120, 43, 247, 109, 147, 82, 119, 91, 52, 12, 98, 144, 170, 8, 33, 247, 97, 115, 76, 89, 177, 171, 51, 167, 206, 176, 199, 142, 104, 191, 248, 168, 202, 231, 87, 47, 63, 90, 123, 80, 82, 44, 249, 130, 173, 50, 78, 162, 252, 178, 58, 201, 104, 45, 132, 214, 141, 103, 128, 22, 149, 7, 186, 109, 250, 200, 53, 28, 157, 139, 158, 163, 235, 211, 242, 148, 110, 73, 230, 37, 193, 202, 56, 210, 44, 224, 44, 120, 199, 217, 248, 91, 224, 119, 213, 1, 30, 145, 5, 201, 170, 228, 231, 175, 137, 56, 64, 71, 126, 255, 195, 200, 0, 250, 25, 12, 82, 108, 136, 146, 163, 50, 142, 151, 120, 161, 201, 165, 164, 22, 223, 164, 194, 154, 165, 217, 75, 231, 79, 125, 233, 109, 155, 160, 245, 105, 121, 28, 199, 169, 155, 227, 222, 244, 79, 86, 49, 134, 30, 102, 194, 104, 238, 17, 210, 62, 225, 78, 104, 43, 139, 59, 83, 5, 9, 115, 149, 208, 135, 109, 189, 248, 227, 45, 51, 243, 135, 78, 236, 38, 99, 206, 132, 217, 195, 9, 227, 65, 137, 89, 116], index: 0 } -[2025-11-18T04:07:30.368502715Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 64581, data_segment = BinString(len=488), data remaining = 467 -[2025-11-18T04:07:30.368535860Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 64581, buffer length = 488, bytes written = 488 -[2025-11-18T04:07:30.368564271Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.368571476Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.368575131Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.368612126Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 14 bytes -[2025-11-18T04:07:30.368618714Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [79, 151, 215, 196, 156, 81, 173, 114, 208, 142, 112, 168, 70, 47] -[2025-11-18T04:07:30.368623573Z TRACE sunset_sftp::sftphandler::sftphandler] Received 14 bytes to process -[2025-11-18T04:07:30.368627268Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.368654742Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 14 -[2025-11-18T04:07:30.368664325Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [79, 151, 215, 196, 156, 81, 173, 114, 208, 142, 112, 168, 70, 47], index: 0 } -[2025-11-18T04:07:30.368673487Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 65069, data_segment = BinString(len=14), data remaining = 453 -[2025-11-18T04:07:30.368753386Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 65069, buffer length = 14, bytes written = 14 -[2025-11-18T04:07:30.368784236Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.368791421Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.368795086Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.368948431Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 512 bytes -[2025-11-18T04:07:30.368981453Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [51, 48, 211, 251, 123, 62, 184, 143, 102, 28, 100, 189, 149, 133, 208, 99, 112, 51, 90, 49, 127, 117, 113, 251, 150, 32, 222, 249, 164, 238, 182, 58, 200, 251, 63, 166, 129, 42, 16, 27, 155, 118, 111, 156, 170, 83, 147, 96, 16, 135, 228, 202, 225, 207, 227, 129, 111, 153, 59, 137, 93, 120, 155, 127, 165, 32, 45, 126, 42, 180, 161, 130, 173, 137, 0, 44, 152, 71, 153, 187, 88, 70, 44, 90, 194, 229, 205, 226, 107, 35, 228, 4, 72, 26, 44, 6, 130, 85, 74, 95, 23, 120, 210, 44, 142, 67, 230, 192, 251, 64, 222, 179, 17, 39, 11, 90, 184, 62, 199, 118, 197, 103, 238, 203, 0, 210, 218, 4, 110, 109, 195, 180, 149, 174, 50, 85, 200, 146, 71, 64, 119, 229, 224, 204, 67, 93, 30, 53, 39, 78, 55, 169, 2, 152, 160, 181, 186, 69, 150, 131, 49, 220, 50, 22, 128, 181, 28, 133, 159, 111, 3, 212, 222, 173, 248, 6, 78, 94, 175, 110, 166, 116, 14, 243, 172, 48, 194, 85, 156, 219, 89, 252, 47, 247, 26, 137, 141, 27, 226, 10, 188, 247, 103, 245, 192, 145, 215, 119, 119, 223, 169, 18, 42, 245, 18, 213, 77, 70, 212, 128, 70, 111, 102, 202, 83, 216, 77, 2, 146, 161, 189, 143, 5, 25, 44, 22, 44, 31, 20, 15, 69, 174, 91, 168, 251, 133, 150, 245, 76, 196, 42, 0, 183, 253, 192, 173, 190, 216, 98, 113, 14, 251, 131, 91, 40, 139, 113, 45, 252, 171, 143, 27, 164, 57, 63, 165, 234, 86, 114, 105, 93, 68, 27, 92, 53, 255, 127, 21, 247, 250, 170, 246, 28, 186, 163, 119, 57, 13, 86, 194, 6, 142, 179, 104, 227, 235, 143, 149, 25, 90, 184, 157, 140, 240, 203, 77, 148, 139, 129, 210, 169, 73, 83, 248, 95, 147, 146, 132, 68, 4, 179, 189, 190, 156, 140, 8, 117, 132, 218, 190, 113, 11, 122, 87, 179, 143, 127, 5, 217, 191, 236, 249, 206, 50, 197, 13, 18, 20, 48, 194, 110, 185, 88, 238, 80, 139, 81, 105, 182, 17, 8, 99, 219, 195, 69, 136, 150, 70, 243, 102, 180, 79, 85, 79, 232, 164, 62, 16, 184, 194, 10, 138, 219, 17, 121, 214, 218, 128, 197, 242, 166, 78, 162, 8, 242, 214, 227, 163, 87, 90, 212, 139, 153, 182, 117, 201, 31, 241, 131, 123, 150, 8, 130, 40, 111, 147, 203, 15, 229, 228, 33, 232, 39, 48, 85, 178, 154, 158, 92, 28, 69, 161, 56, 9, 7, 252, 171, 208, 98, 144, 55, 171, 20, 0, 0, 4, 25, 6, 0, 0, 0, 12, 0, 0, 0, 4, 215, 124, 9, 14, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 38, 28, 151, 7, 83, 85, 208, 31, 140, 39, 122, 77, 215, 209, 166, 8, 75, 127, 195, 6, 17, 127, 158, 6, 93, 66, 218, 212, 56, 12] -[2025-11-18T04:07:30.369041722Z TRACE sunset_sftp::sftphandler::sftphandler] Received 512 bytes to process -[2025-11-18T04:07:30.369050328Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.369054147Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 512 -[2025-11-18T04:07:30.369076896Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [51, 48, 211, 251, 123, 62, 184, 143, 102, 28, 100, 189, 149, 133, 208, 99, 112, 51, 90, 49, 127, 117, 113, 251, 150, 32, 222, 249, 164, 238, 182, 58, 200, 251, 63, 166, 129, 42, 16, 27, 155, 118, 111, 156, 170, 83, 147, 96, 16, 135, 228, 202, 225, 207, 227, 129, 111, 153, 59, 137, 93, 120, 155, 127, 165, 32, 45, 126, 42, 180, 161, 130, 173, 137, 0, 44, 152, 71, 153, 187, 88, 70, 44, 90, 194, 229, 205, 226, 107, 35, 228, 4, 72, 26, 44, 6, 130, 85, 74, 95, 23, 120, 210, 44, 142, 67, 230, 192, 251, 64, 222, 179, 17, 39, 11, 90, 184, 62, 199, 118, 197, 103, 238, 203, 0, 210, 218, 4, 110, 109, 195, 180, 149, 174, 50, 85, 200, 146, 71, 64, 119, 229, 224, 204, 67, 93, 30, 53, 39, 78, 55, 169, 2, 152, 160, 181, 186, 69, 150, 131, 49, 220, 50, 22, 128, 181, 28, 133, 159, 111, 3, 212, 222, 173, 248, 6, 78, 94, 175, 110, 166, 116, 14, 243, 172, 48, 194, 85, 156, 219, 89, 252, 47, 247, 26, 137, 141, 27, 226, 10, 188, 247, 103, 245, 192, 145, 215, 119, 119, 223, 169, 18, 42, 245, 18, 213, 77, 70, 212, 128, 70, 111, 102, 202, 83, 216, 77, 2, 146, 161, 189, 143, 5, 25, 44, 22, 44, 31, 20, 15, 69, 174, 91, 168, 251, 133, 150, 245, 76, 196, 42, 0, 183, 253, 192, 173, 190, 216, 98, 113, 14, 251, 131, 91, 40, 139, 113, 45, 252, 171, 143, 27, 164, 57, 63, 165, 234, 86, 114, 105, 93, 68, 27, 92, 53, 255, 127, 21, 247, 250, 170, 246, 28, 186, 163, 119, 57, 13, 86, 194, 6, 142, 179, 104, 227, 235, 143, 149, 25, 90, 184, 157, 140, 240, 203, 77, 148, 139, 129, 210, 169, 73, 83, 248, 95, 147, 146, 132, 68, 4, 179, 189, 190, 156, 140, 8, 117, 132, 218, 190, 113, 11, 122, 87, 179, 143, 127, 5, 217, 191, 236, 249, 206, 50, 197, 13, 18, 20, 48, 194, 110, 185, 88, 238, 80, 139, 81, 105, 182, 17, 8, 99, 219, 195, 69, 136, 150, 70, 243, 102, 180, 79, 85, 79, 232, 164, 62, 16, 184, 194, 10, 138, 219, 17, 121, 214, 218, 128, 197, 242, 166, 78, 162, 8, 242, 214, 227, 163, 87, 90, 212, 139, 153, 182, 117, 201, 31, 241, 131, 123, 150, 8, 130, 40, 111, 147, 203, 15, 229, 228, 33, 232, 39, 48, 85, 178, 154, 158, 92, 28, 69, 161, 56, 9, 7, 252, 171, 208, 98, 144, 55, 171, 20, 0, 0, 4, 25, 6, 0, 0, 0, 12, 0, 0, 0, 4, 215, 124, 9, 14, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 38, 28, 151, 7, 83, 85, 208, 31, 140, 39, 122, 77, 215, 209, 166, 8, 75, 127, 195, 6, 17, 127, 158, 6, 93, 66, 218, 212, 56, 12], index: 0 } -[2025-11-18T04:07:30.369135735Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 65083, data_segment = BinString(len=453), data remaining = 0 -[2025-11-18T04:07:30.369175273Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 65083, buffer length = 453, bytes written = 453 -[2025-11-18T04:07:30.369190785Z INFO sunset_sftp::sftphandler::sftphandler] Finished multi part Write Request -[2025-11-18T04:07:30.369213040Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 59 -[2025-11-18T04:07:30.369220102Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 59 -[2025-11-18T04:07:30.369224219Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 59 -[2025-11-18T04:07:30.369231342Z WARN sunset_sftp::sftphandler::sftphandler] RanOut for the SFTP Packet in the source buffer: RanOut -[2025-11-18T04:07:30.369237343Z DEBUG sunset_sftp::sftphandler::sftphandler] Handing Ran out -[2025-11-18T04:07:30.369240936Z DEBUG sunset_sftp::sftphandler::sftphandler] about to decode packet partial write content. Source remaining = 30 -[2025-11-18T04:07:30.369263541Z TRACE sunset_sftp::sftphandler::sftphandler] obscured_file_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, req_id = ReqId(12), offset = 65536, data_in_buffer = BinString(len=30), write_tracker = PartialWriteRequestTracker { req_id: ReqId(12), opaque_handle: DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, remain_data_len: 994, remain_data_offset: 65566 } -[2025-11-18T04:07:30.369340424Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 65536, buffer length = 30, bytes written = 30 -[2025-11-18T04:07:30.369370214Z DEBUG sunset_sftp::sftphandler::sftphandler] Storing a write tracker for a fragmented write request -[2025-11-18T04:07:30.369377760Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.369381640Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.369385377Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.369407447Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.369426902Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 488 bytes -[2025-11-18T04:07:30.369434344Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [43, 55, 242, 47, 60, 147, 5, 18, 108, 226, 171, 31, 250, 134, 222, 88, 76, 18, 140, 139, 149, 232, 117, 190, 108, 79, 56, 233, 54, 103, 151, 155, 205, 115, 56, 64, 125, 215, 131, 193, 19, 235, 187, 62, 122, 202, 67, 106, 202, 176, 8, 166, 97, 192, 98, 233, 183, 176, 101, 151, 34, 94, 88, 158, 124, 92, 59, 162, 124, 42, 84, 163, 159, 153, 89, 183, 17, 80, 90, 208, 121, 44, 98, 90, 234, 161, 214, 153, 182, 129, 199, 61, 48, 225, 184, 134, 84, 31, 218, 9, 70, 10, 169, 22, 148, 205, 229, 245, 191, 103, 33, 248, 164, 79, 183, 60, 97, 40, 205, 135, 36, 92, 210, 36, 51, 48, 222, 160, 34, 171, 94, 3, 107, 196, 119, 104, 214, 186, 93, 2, 198, 137, 161, 252, 237, 103, 31, 152, 123, 147, 68, 38, 116, 172, 152, 98, 63, 112, 225, 112, 195, 216, 142, 18, 15, 201, 222, 138, 9, 189, 255, 56, 244, 113, 202, 217, 52, 223, 59, 67, 216, 147, 137, 167, 28, 253, 149, 78, 161, 231, 117, 151, 2, 5, 12, 90, 247, 201, 243, 237, 236, 64, 229, 53, 198, 248, 250, 87, 113, 247, 237, 50, 232, 95, 140, 16, 231, 6, 182, 157, 250, 57, 128, 217, 42, 0, 82, 218, 88, 238, 182, 96, 105, 6, 245, 0, 184, 139, 121, 85, 43, 144, 23, 164, 136, 15, 224, 9, 127, 228, 148, 104, 196, 14, 79, 187, 139, 128, 100, 102, 4, 15, 155, 22, 13, 133, 116, 67, 192, 249, 126, 96, 133, 14, 98, 22, 124, 17, 36, 207, 206, 195, 143, 180, 54, 31, 111, 190, 193, 250, 19, 70, 47, 86, 70, 170, 227, 164, 9, 186, 26, 3, 148, 1, 83, 111, 234, 57, 144, 198, 82, 141, 15, 65, 239, 71, 85, 182, 87, 45, 19, 252, 18, 244, 212, 40, 170, 155, 124, 71, 109, 242, 40, 198, 39, 75, 230, 215, 179, 76, 186, 192, 165, 34, 87, 155, 108, 22, 129, 62, 178, 239, 88, 229, 113, 194, 133, 134, 108, 239, 161, 90, 234, 184, 100, 124, 58, 124, 189, 14, 195, 242, 88, 170, 182, 224, 239, 54, 117, 94, 71, 240, 9, 0, 240, 249, 36, 80, 180, 134, 9, 139, 22, 4, 71, 132, 42, 95, 103, 132, 203, 60, 33, 214, 1, 33, 116, 98, 71, 168, 49, 166, 103, 103, 221, 147, 159, 128, 54, 147, 110, 3, 163, 122, 106, 118, 239, 224, 233, 153, 28, 227, 22, 195, 141, 7, 131, 239, 164, 148, 97, 52, 17, 181, 126, 252, 187, 188, 62, 27, 36, 28, 29, 81, 173, 28, 31, 245, 127, 214, 172, 170, 104, 116, 4, 45, 176, 188, 148, 148, 177, 84, 21, 159, 161, 92, 159, 107, 40, 119, 130, 160, 70, 129, 209, 192, 106, 72] -[2025-11-18T04:07:30.369488540Z TRACE sunset_sftp::sftphandler::sftphandler] Received 488 bytes to process -[2025-11-18T04:07:30.369495365Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.369499060Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 488 -[2025-11-18T04:07:30.369503363Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [43, 55, 242, 47, 60, 147, 5, 18, 108, 226, 171, 31, 250, 134, 222, 88, 76, 18, 140, 139, 149, 232, 117, 190, 108, 79, 56, 233, 54, 103, 151, 155, 205, 115, 56, 64, 125, 215, 131, 193, 19, 235, 187, 62, 122, 202, 67, 106, 202, 176, 8, 166, 97, 192, 98, 233, 183, 176, 101, 151, 34, 94, 88, 158, 124, 92, 59, 162, 124, 42, 84, 163, 159, 153, 89, 183, 17, 80, 90, 208, 121, 44, 98, 90, 234, 161, 214, 153, 182, 129, 199, 61, 48, 225, 184, 134, 84, 31, 218, 9, 70, 10, 169, 22, 148, 205, 229, 245, 191, 103, 33, 248, 164, 79, 183, 60, 97, 40, 205, 135, 36, 92, 210, 36, 51, 48, 222, 160, 34, 171, 94, 3, 107, 196, 119, 104, 214, 186, 93, 2, 198, 137, 161, 252, 237, 103, 31, 152, 123, 147, 68, 38, 116, 172, 152, 98, 63, 112, 225, 112, 195, 216, 142, 18, 15, 201, 222, 138, 9, 189, 255, 56, 244, 113, 202, 217, 52, 223, 59, 67, 216, 147, 137, 167, 28, 253, 149, 78, 161, 231, 117, 151, 2, 5, 12, 90, 247, 201, 243, 237, 236, 64, 229, 53, 198, 248, 250, 87, 113, 247, 237, 50, 232, 95, 140, 16, 231, 6, 182, 157, 250, 57, 128, 217, 42, 0, 82, 218, 88, 238, 182, 96, 105, 6, 245, 0, 184, 139, 121, 85, 43, 144, 23, 164, 136, 15, 224, 9, 127, 228, 148, 104, 196, 14, 79, 187, 139, 128, 100, 102, 4, 15, 155, 22, 13, 133, 116, 67, 192, 249, 126, 96, 133, 14, 98, 22, 124, 17, 36, 207, 206, 195, 143, 180, 54, 31, 111, 190, 193, 250, 19, 70, 47, 86, 70, 170, 227, 164, 9, 186, 26, 3, 148, 1, 83, 111, 234, 57, 144, 198, 82, 141, 15, 65, 239, 71, 85, 182, 87, 45, 19, 252, 18, 244, 212, 40, 170, 155, 124, 71, 109, 242, 40, 198, 39, 75, 230, 215, 179, 76, 186, 192, 165, 34, 87, 155, 108, 22, 129, 62, 178, 239, 88, 229, 113, 194, 133, 134, 108, 239, 161, 90, 234, 184, 100, 124, 58, 124, 189, 14, 195, 242, 88, 170, 182, 224, 239, 54, 117, 94, 71, 240, 9, 0, 240, 249, 36, 80, 180, 134, 9, 139, 22, 4, 71, 132, 42, 95, 103, 132, 203, 60, 33, 214, 1, 33, 116, 98, 71, 168, 49, 166, 103, 103, 221, 147, 159, 128, 54, 147, 110, 3, 163, 122, 106, 118, 239, 224, 233, 153, 28, 227, 22, 195, 141, 7, 131, 239, 164, 148, 97, 52, 17, 181, 126, 252, 187, 188, 62, 27, 36, 28, 29, 81, 173, 28, 31, 245, 127, 214, 172, 170, 104, 116, 4, 45, 176, 188, 148, 148, 177, 84, 21, 159, 161, 92, 159, 107, 40, 119, 130, 160, 70, 129, 209, 192, 106, 72], index: 0 } -[2025-11-18T04:07:30.369557096Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 65566, data_segment = BinString(len=488), data remaining = 506 -[2025-11-18T04:07:30.369613577Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 65566, buffer length = 488, bytes written = 488 -[2025-11-18T04:07:30.369643882Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.369651088Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.369654845Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.369682967Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 42 bytes -[2025-11-18T04:07:30.369708938Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [115, 168, 108, 51, 193, 52, 19, 31, 170, 36, 253, 14, 65, 50, 49, 88, 211, 185, 67, 14, 180, 123, 6, 104, 161, 36, 215, 59, 30, 39, 183, 67, 103, 153, 2, 76, 248, 171, 77, 162, 79, 167] -[2025-11-18T04:07:30.369740776Z TRACE sunset_sftp::sftphandler::sftphandler] Received 42 bytes to process -[2025-11-18T04:07:30.369748733Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.369752439Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 42 -[2025-11-18T04:07:30.369756762Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [115, 168, 108, 51, 193, 52, 19, 31, 170, 36, 253, 14, 65, 50, 49, 88, 211, 185, 67, 14, 180, 123, 6, 104, 161, 36, 215, 59, 30, 39, 183, 67, 103, 153, 2, 76, 248, 171, 77, 162, 79, 167], index: 0 } -[2025-11-18T04:07:30.369765399Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 66054, data_segment = BinString(len=42), data remaining = 464 -[2025-11-18T04:07:30.369796517Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 66054, buffer length = 42, bytes written = 42 -[2025-11-18T04:07:30.369826502Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.369833800Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.369837475Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.369920432Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 464 bytes -[2025-11-18T04:07:30.369949090Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [144, 252, 138, 11, 49, 93, 92, 70, 91, 200, 15, 151, 171, 209, 193, 224, 198, 108, 107, 33, 29, 247, 54, 131, 42, 129, 145, 69, 168, 65, 246, 87, 69, 140, 137, 171, 22, 223, 208, 133, 172, 54, 208, 168, 11, 51, 51, 179, 124, 207, 104, 39, 26, 140, 189, 192, 72, 134, 18, 86, 242, 95, 119, 47, 243, 30, 227, 180, 119, 13, 139, 55, 115, 92, 38, 251, 204, 89, 227, 98, 170, 32, 2, 125, 129, 13, 111, 180, 77, 82, 18, 191, 146, 148, 39, 244, 34, 36, 15, 90, 60, 66, 118, 143, 150, 42, 246, 59, 152, 69, 178, 174, 173, 41, 41, 68, 17, 226, 96, 133, 142, 228, 252, 133, 80, 178, 183, 235, 54, 61, 148, 149, 158, 75, 241, 91, 229, 204, 120, 136, 66, 74, 158, 61, 38, 251, 175, 36, 236, 179, 192, 212, 53, 193, 162, 84, 244, 35, 85, 102, 36, 144, 242, 62, 10, 150, 161, 26, 156, 177, 244, 27, 58, 164, 187, 51, 126, 214, 2, 200, 186, 136, 58, 175, 113, 104, 38, 80, 155, 57, 11, 74, 72, 98, 199, 24, 249, 108, 59, 102, 33, 160, 171, 168, 13, 144, 143, 16, 65, 55, 135, 101, 233, 213, 228, 138, 189, 140, 16, 90, 33, 198, 133, 32, 183, 7, 154, 61, 57, 43, 241, 29, 43, 191, 50, 246, 249, 50, 177, 21, 49, 237, 228, 249, 224, 33, 213, 95, 81, 35, 23, 33, 208, 114, 41, 183, 18, 19, 185, 2, 127, 224, 158, 228, 32, 244, 0, 38, 240, 149, 51, 113, 46, 55, 17, 35, 50, 39, 139, 115, 238, 30, 74, 26, 226, 56, 57, 178, 159, 49, 61, 204, 98, 27, 123, 37, 131, 102, 179, 61, 36, 221, 15, 14, 51, 100, 221, 89, 122, 82, 172, 98, 143, 114, 159, 181, 153, 235, 235, 94, 217, 5, 92, 123, 66, 186, 40, 184, 153, 74, 229, 109, 251, 39, 206, 18, 151, 65, 191, 233, 187, 105, 71, 197, 178, 252, 152, 113, 179, 236, 147, 105, 8, 123, 95, 238, 190, 244, 251, 11, 87, 114, 178, 3, 240, 183, 3, 171, 171, 17, 213, 63, 74, 239, 129, 235, 127, 198, 35, 210, 87, 233, 28, 225, 194, 57, 27, 50, 1, 181, 87, 118, 206, 140, 130, 230, 134, 190, 166, 172, 98, 229, 152, 112, 114, 177, 122, 102, 255, 55, 31, 52, 28, 237, 35, 205, 67, 103, 159, 224, 134, 181, 42, 59, 142, 12, 30, 121, 177, 101, 146, 78, 249, 246, 175, 107, 205, 154, 36, 230, 89, 16, 110, 26, 87, 247, 195, 243, 39, 245, 190, 176, 241, 153, 183, 18, 146, 245, 68, 239, 209, 23, 63, 62] -[2025-11-18T04:07:30.370005942Z TRACE sunset_sftp::sftphandler::sftphandler] Received 464 bytes to process -[2025-11-18T04:07:30.370012622Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.370016369Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Fragmented(ProcessingLongRequest) ]=======================> Buffer remaining: 464 -[2025-11-18T04:07:30.370024100Z TRACE sunset_sftp::sftphandler::sftphandler] Source content: SftpSource { buffer: [144, 252, 138, 11, 49, 93, 92, 70, 91, 200, 15, 151, 171, 209, 193, 224, 198, 108, 107, 33, 29, 247, 54, 131, 42, 129, 145, 69, 168, 65, 246, 87, 69, 140, 137, 171, 22, 223, 208, 133, 172, 54, 208, 168, 11, 51, 51, 179, 124, 207, 104, 39, 26, 140, 189, 192, 72, 134, 18, 86, 242, 95, 119, 47, 243, 30, 227, 180, 119, 13, 139, 55, 115, 92, 38, 251, 204, 89, 227, 98, 170, 32, 2, 125, 129, 13, 111, 180, 77, 82, 18, 191, 146, 148, 39, 244, 34, 36, 15, 90, 60, 66, 118, 143, 150, 42, 246, 59, 152, 69, 178, 174, 173, 41, 41, 68, 17, 226, 96, 133, 142, 228, 252, 133, 80, 178, 183, 235, 54, 61, 148, 149, 158, 75, 241, 91, 229, 204, 120, 136, 66, 74, 158, 61, 38, 251, 175, 36, 236, 179, 192, 212, 53, 193, 162, 84, 244, 35, 85, 102, 36, 144, 242, 62, 10, 150, 161, 26, 156, 177, 244, 27, 58, 164, 187, 51, 126, 214, 2, 200, 186, 136, 58, 175, 113, 104, 38, 80, 155, 57, 11, 74, 72, 98, 199, 24, 249, 108, 59, 102, 33, 160, 171, 168, 13, 144, 143, 16, 65, 55, 135, 101, 233, 213, 228, 138, 189, 140, 16, 90, 33, 198, 133, 32, 183, 7, 154, 61, 57, 43, 241, 29, 43, 191, 50, 246, 249, 50, 177, 21, 49, 237, 228, 249, 224, 33, 213, 95, 81, 35, 23, 33, 208, 114, 41, 183, 18, 19, 185, 2, 127, 224, 158, 228, 32, 244, 0, 38, 240, 149, 51, 113, 46, 55, 17, 35, 50, 39, 139, 115, 238, 30, 74, 26, 226, 56, 57, 178, 159, 49, 61, 204, 98, 27, 123, 37, 131, 102, 179, 61, 36, 221, 15, 14, 51, 100, 221, 89, 122, 82, 172, 98, 143, 114, 159, 181, 153, 235, 235, 94, 217, 5, 92, 123, 66, 186, 40, 184, 153, 74, 229, 109, 251, 39, 206, 18, 151, 65, 191, 233, 187, 105, 71, 197, 178, 252, 152, 113, 179, 236, 147, 105, 8, 123, 95, 238, 190, 244, 251, 11, 87, 114, 178, 3, 240, 183, 3, 171, 171, 17, 213, 63, 74, 239, 129, 235, 127, 198, 35, 210, 87, 233, 28, 225, 194, 57, 27, 50, 1, 181, 87, 118, 206, 140, 130, 230, 134, 190, 166, 172, 98, 229, 152, 112, 114, 177, 122, 102, 255, 55, 31, 52, 28, 237, 35, 205, 67, 103, 159, 224, 134, 181, 42, 59, 142, 12, 30, 121, 177, 101, 146, 78, 249, 246, 175, 107, 205, 154, 36, 230, 89, 16, 110, 26, 87, 247, 195, 243, 39, 245, 190, 176, 241, 153, 183, 18, 146, 245, 68, 239, 209, 23, 63, 62], index: 0 } -[2025-11-18T04:07:30.370074055Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing successive chunks of a long write packet. Writing : opaque_handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, write_offset = 66096, data_segment = BinString(len=464), data remaining = 0 -[2025-11-18T04:07:30.370107550Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Write operation: handle = DemoOpaqueFileHandle { tiny_hash: [215, 124, 9, 14] }, filepath = "./demo/sftp/std/testing/out/65kB_random", offset = 66096, buffer length = 464, bytes written = 464 -[2025-11-18T04:07:30.370140253Z INFO sunset_sftp::sftphandler::sftphandler] Finished multi part Write Request -[2025-11-18T04:07:30.370166348Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.370193647Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.370220554Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.414834521Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes -[2025-11-18T04:07:30.414888388Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 9, 0, 0, 0, 4, 215, 124, 9, 14] -[2025-11-18T04:07:30.414897930Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process -[2025-11-18T04:07:30.414901863Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.414905599Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 -[2025-11-18T04:07:30.414910478Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 -[2025-11-18T04:07:30.414941452Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(9), Close { handle: FileHandle(BinString(len=4)) }) -[2025-11-18T04:07:30.414973723Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/65kB_random" was successful -[2025-11-18T04:07:30.415018140Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.415049062Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.415056278Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.415060097Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.415629821Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 0 bytes -[2025-11-18T04:07:30.415667815Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [] -[2025-11-18T04:07:30.415675515Z DEBUG sunset_sftp::sftphandler::sftphandler] client disconnected -[2025-11-18T04:07:30.415679293Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing returned: Err(ClientDisconnected) -[2025-11-18T04:07:30.415686128Z WARN sunset_demo_sftp_std] sftp_loop finished: Err(ChannelEOF) -[2025-11-18T04:07:30.415693045Z WARN sunset_demo_common::server] Ended with error ChannelEOF -[2025-11-18T04:07:30.415799060Z INFO sunset_demo_common::server] Listening on TCP:22... -[2025-11-18T04:07:30.444787113Z INFO sunset_demo_common::server] Connection from 192.168.69.100:45016 -[2025-11-18T04:07:30.444866539Z INFO sunset_demo_sftp_std] prog_loop started -[2025-11-18T04:07:30.473277794Z INFO sunset_demo_common::server] Allowing auth for user any -[2025-11-18T04:07:30.475576502Z WARN sunset::channel] Unknown channel req type "env" -[2025-11-18T04:07:30.475678996Z INFO sunset_demo_sftp_std] Starting 'sftp' subsystem -[2025-11-18T04:07:30.475762416Z INFO sunset_demo_sftp_std] SFTP loop has received a channel handle ChanNum(0) -[2025-11-18T04:07:30.475823602Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.475911746Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 9 bytes -[2025-11-18T04:07:30.475967919Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 5, 1, 0, 0, 0, 3] -[2025-11-18T04:07:30.476025265Z TRACE sunset_sftp::sftphandler::sftphandler] Received 9 bytes to process -[2025-11-18T04:07:30.476041055Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.476099441Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Initializing ]=======================> Buffer remaining: 9 -[2025-11-18T04:07:30.476118824Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 9 -[2025-11-18T04:07:30.476173978Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.476229100Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.476416003Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.522213652Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 14 bytes -[2025-11-18T04:07:30.522288240Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 10, 16, 0, 0, 0, 1, 0, 0, 0, 1, 46] -[2025-11-18T04:07:30.522311205Z TRACE sunset_sftp::sftphandler::sftphandler] Received 14 bytes to process -[2025-11-18T04:07:30.522320047Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.522328292Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 14 -[2025-11-18T04:07:30.522381006Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 14 -[2025-11-18T04:07:30.522442943Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: PathInfo(ReqId(1), PathInfo { path: TextString(".") }) -[2025-11-18T04:07:30.522463562Z INFO sunset_demo_sftp_std::demosftpserver] finding path for: "." -[2025-11-18T04:07:30.522478652Z DEBUG sunset_demo_sftp_std::demosftpserver] Will return: NameEntry { filename: Filename(TextString("./demo/sftp/std/testing/out/")), _longname: Filename(TextString("")), attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } } -[2025-11-18T04:07:30.522501411Z DEBUG sunset_sftp::sftphandler::sftphandler] PathInfo encoded length: 40 -[2025-11-18T04:07:30.522514309Z TRACE sunset_sftp::sftphandler::sftphandler] PathInfo Response content: 40 -[2025-11-18T04:07:30.522528391Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.522574733Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.522590472Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.522598738Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.523406710Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes -[2025-11-18T04:07:30.523448306Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 2, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, 109] -[2025-11-18T04:07:30.523460638Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process -[2025-11-18T04:07:30.523464519Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.523468214Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 -[2025-11-18T04:07:30.523495215Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:07:30.523525118Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(2), LStat { file_path: TextString("./demo/sftp/std/testing/out/./512B_random") }) -[2025-11-18T04:07:30.523539622Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./512B_random" -[2025-11-18T04:07:30.523563112Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./512B_random" is Attrs { size: Some(512), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438849), ext_count: None } -[2025-11-18T04:07:30.523593499Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.523600653Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.523604276Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.523607889Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.524163861Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes -[2025-11-18T04:07:30.524204675Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 3, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, 109] -[2025-11-18T04:07:30.524217059Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process -[2025-11-18T04:07:30.524221104Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.524225057Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 -[2025-11-18T04:07:30.524230369Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:07:30.524260478Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(3), Stat { file_path: TextString("./demo/sftp/std/testing/out/./512B_random") }) -[2025-11-18T04:07:30.524295311Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./512B_random" -[2025-11-18T04:07:30.524328292Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./512B_random" is Attrs { size: Some(512), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438849), ext_count: None } -[2025-11-18T04:07:30.524358340Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.524365411Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.524370023Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.524374789Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.525025730Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes -[2025-11-18T04:07:30.525065330Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 4, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] -[2025-11-18T04:07:30.525077909Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process -[2025-11-18T04:07:30.525081810Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.525085454Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 -[2025-11-18T04:07:30.525089849Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 -[2025-11-18T04:07:30.525119032Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(4), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./512B_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) -[2025-11-18T04:07:30.525154628Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./512B_random", mode = SSH_FXF_READ -[2025-11-18T04:07:30.525181473Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false -[2025-11-18T04:07:30.525242556Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./512B_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }) -[2025-11-18T04:07:30.525273242Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.525280591Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.525284276Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.525306634Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.526078773Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-18T04:07:30.526119083Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 5, 0, 0, 0, 4, 55, 15, 63, 111, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] -[2025-11-18T04:07:30.526128976Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process -[2025-11-18T04:07:30.526132887Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.526136562Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:07:30.526141256Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:07:30.526147288Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(5), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) -[2025-11-18T04:07:30.526176018Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }, filepath = "./demo/sftp/std/testing/out/./512B_random", offset = 0, len = 32768 -[2025-11-18T04:07:30.526215443Z WARN sunset_demo_sftp_std::demosftpserver] Read operation: length + offset > file length. Clipping ( 32768 + 0 > 512) -[2025-11-18T04:07:30.526226241Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 512 -[2025-11-18T04:07:30.526258728Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data -[2025-11-18T04:07:30.526282496Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.526289660Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.526293808Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.526322569Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.570690246Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-18T04:07:30.570758411Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 6, 0, 0, 0, 4, 55, 15, 63, 111, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 126, 0] -[2025-11-18T04:07:30.570779338Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process -[2025-11-18T04:07:30.570788046Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.570837775Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:07:30.570858259Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:07:30.570869366Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(6), Read { handle: FileHandle(BinString(len=4)), offset: 512, len: 32256 }) -[2025-11-18T04:07:30.570922039Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }, filepath = "./demo/sftp/std/testing/out/./512B_random", offset = 512, len = 32256 -[2025-11-18T04:07:30.570998398Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF -[2025-11-18T04:07:30.571046304Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.571060160Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.571064617Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.571069197Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.571566825Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes -[2025-11-18T04:07:30.571598231Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 7, 0, 0, 0, 4, 55, 15, 63, 111] -[2025-11-18T04:07:30.571607269Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process -[2025-11-18T04:07:30.571611077Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.571614721Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 -[2025-11-18T04:07:30.571619343Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 -[2025-11-18T04:07:30.571644779Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(7), Close { handle: FileHandle(BinString(len=4)) }) -[2025-11-18T04:07:30.571674342Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./512B_random" was successful -[2025-11-18T04:07:30.571691172Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.571714930Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.571722568Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.571726305Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.572166370Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes -[2025-11-18T04:07:30.572196334Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 8, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109] -[2025-11-18T04:07:30.572207863Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process -[2025-11-18T04:07:30.572211713Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.572215378Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 -[2025-11-18T04:07:30.572219475Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:07:30.572244354Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(8), LStat { file_path: TextString("./demo/sftp/std/testing/out/./16kB_random") }) -[2025-11-18T04:07:30.572273908Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./16kB_random" -[2025-11-18T04:07:30.572315957Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./16kB_random" is Attrs { size: Some(16384), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438849), ext_count: None } -[2025-11-18T04:07:30.572350204Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.572357379Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.572361074Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.572383896Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.572882758Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes -[2025-11-18T04:07:30.572913485Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 9, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109] -[2025-11-18T04:07:30.572924705Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process -[2025-11-18T04:07:30.572928544Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.572932209Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 -[2025-11-18T04:07:30.572936326Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:07:30.572961597Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(9), Stat { file_path: TextString("./demo/sftp/std/testing/out/./16kB_random") }) -[2025-11-18T04:07:30.572990306Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./16kB_random" -[2025-11-18T04:07:30.573027240Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./16kB_random" is Attrs { size: Some(16384), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438849), ext_count: None } -[2025-11-18T04:07:30.573057524Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.573064997Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.573090289Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.573098112Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.573531692Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes -[2025-11-18T04:07:30.573561255Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 10, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] -[2025-11-18T04:07:30.573573361Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process -[2025-11-18T04:07:30.573577241Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.573580916Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 -[2025-11-18T04:07:30.573585034Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 -[2025-11-18T04:07:30.573609934Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(10), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./16kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) -[2025-11-18T04:07:30.573641103Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./16kB_random", mode = SSH_FXF_READ -[2025-11-18T04:07:30.573668485Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false -[2025-11-18T04:07:30.573713365Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./16kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }) -[2025-11-18T04:07:30.573744678Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.573751905Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.573755600Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.573778133Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.574301062Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-18T04:07:30.574331840Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 11, 0, 0, 0, 4, 204, 179, 134, 195, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] -[2025-11-18T04:07:30.574341609Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process -[2025-11-18T04:07:30.574345582Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.574349267Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:07:30.574353395Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:07:30.574378398Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(11), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) -[2025-11-18T04:07:30.574407169Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }, filepath = "./demo/sftp/std/testing/out/./16kB_random", offset = 0, len = 32768 -[2025-11-18T04:07:30.574441725Z WARN sunset_demo_sftp_std::demosftpserver] Read operation: length + offset > file length. Clipping ( 32768 + 0 > 16384) -[2025-11-18T04:07:30.574468633Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 16384 -[2025-11-18T04:07:30.620633925Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data -[2025-11-18T04:07:30.620676819Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.620684745Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.620689017Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.620692846Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.621652731Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-18T04:07:30.621688409Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 12, 0, 0, 0, 4, 204, 179, 134, 195, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 64, 0] -[2025-11-18T04:07:30.621699990Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process -[2025-11-18T04:07:30.621703942Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.621707792Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:07:30.621731663Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:07:30.621760712Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(12), Read { handle: FileHandle(BinString(len=4)), offset: 16384, len: 16384 }) -[2025-11-18T04:07:30.621790399Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }, filepath = "./demo/sftp/std/testing/out/./16kB_random", offset = 16384, len = 16384 -[2025-11-18T04:07:30.621810307Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF -[2025-11-18T04:07:30.621836051Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.621861816Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.621888745Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.621914201Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.622335882Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes -[2025-11-18T04:07:30.622365929Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 13, 0, 0, 0, 4, 204, 179, 134, 195] -[2025-11-18T04:07:30.622374833Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process -[2025-11-18T04:07:30.622378662Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.622382296Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 -[2025-11-18T04:07:30.622386444Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 -[2025-11-18T04:07:30.622410665Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(13), Close { handle: FileHandle(BinString(len=4)) }) -[2025-11-18T04:07:30.622439848Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./16kB_random" was successful -[2025-11-18T04:07:30.622476442Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.622486149Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.622509031Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.622517318Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.622914952Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes -[2025-11-18T04:07:30.622945092Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 14, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109] -[2025-11-18T04:07:30.622956673Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process -[2025-11-18T04:07:30.622960523Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.622964177Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 -[2025-11-18T04:07:30.622968335Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:07:30.622992855Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(14), LStat { file_path: TextString("./demo/sftp/std/testing/out/./64kB_random") }) -[2025-11-18T04:07:30.623021646Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./64kB_random" -[2025-11-18T04:07:30.623062553Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./64kB_random" is Attrs { size: Some(65536), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438850), ext_count: None } -[2025-11-18T04:07:30.623096893Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.623104160Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.623126127Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.623132911Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.623499520Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes -[2025-11-18T04:07:30.623532913Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 15, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109] -[2025-11-18T04:07:30.623544112Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process -[2025-11-18T04:07:30.623547952Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.623571576Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 -[2025-11-18T04:07:30.623598504Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:07:30.623625278Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(15), Stat { file_path: TextString("./demo/sftp/std/testing/out/./64kB_random") }) -[2025-11-18T04:07:30.623654831Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./64kB_random" -[2025-11-18T04:07:30.623672289Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./64kB_random" is Attrs { size: Some(65536), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438850), ext_count: None } -[2025-11-18T04:07:30.623683067Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.623707988Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.623715018Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.623719836Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.624113147Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes -[2025-11-18T04:07:30.624143163Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 16, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] -[2025-11-18T04:07:30.624154744Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process -[2025-11-18T04:07:30.624178995Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.624205512Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 -[2025-11-18T04:07:30.624231473Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 -[2025-11-18T04:07:30.624260243Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(16), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./64kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) -[2025-11-18T04:07:30.624274058Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./64kB_random", mode = SSH_FXF_READ -[2025-11-18T04:07:30.624299061Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false -[2025-11-18T04:07:30.624321872Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./64kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }) -[2025-11-18T04:07:30.624347946Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.624355295Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.624380669Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.624388369Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.624962890Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-18T04:07:30.624993740Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 17, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] -[2025-11-18T04:07:30.625003138Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process -[2025-11-18T04:07:30.625007019Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.625010684Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:07:30.625014811Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:07:30.625040237Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(17), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) -[2025-11-18T04:07:30.625068658Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 0, len = 32768 -[2025-11-18T04:07:30.625104974Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 -[2025-11-18T04:07:30.676397692Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data -[2025-11-18T04:07:30.676439834Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.676447544Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.676451755Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.676455543Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.677366008Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 58 bytes -[2025-11-18T04:07:30.677396302Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 18, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 25, 5, 0, 0, 0, 19, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 128, 0] -[2025-11-18T04:07:30.677409509Z TRACE sunset_sftp::sftphandler::sftphandler] Received 58 bytes to process -[2025-11-18T04:07:30.677413359Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.677416982Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 58 -[2025-11-18T04:07:30.677421337Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 58 -[2025-11-18T04:07:30.677427328Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(18), Read { handle: FileHandle(BinString(len=4)), offset: 32768, len: 32768 }) -[2025-11-18T04:07:30.677434286Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 32768, len = 32768 -[2025-11-18T04:07:30.677449150Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 -[2025-11-18T04:07:30.725418534Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data -[2025-11-18T04:07:30.725464310Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 29 bytes -[2025-11-18T04:07:30.725473574Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 29 -[2025-11-18T04:07:30.725477558Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:07:30.725482005Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:07:30.725488088Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(19), Read { handle: FileHandle(BinString(len=4)), offset: 65536, len: 32768 }) -[2025-11-18T04:07:30.725516324Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 65536, len = 32768 -[2025-11-18T04:07:30.725556500Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF -[2025-11-18T04:07:30.725565167Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.725569089Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.725572733Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.725579620Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.726220195Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes -[2025-11-18T04:07:30.726251694Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 20, 0, 0, 0, 4, 195, 17, 206, 102] -[2025-11-18T04:07:30.726260701Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process -[2025-11-18T04:07:30.726264551Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.726268195Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 -[2025-11-18T04:07:30.726272703Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 -[2025-11-18T04:07:30.726277840Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(20), Close { handle: FileHandle(BinString(len=4)) }) -[2025-11-18T04:07:30.726284839Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./64kB_random" was successful -[2025-11-18T04:07:30.726296965Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.726321732Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.726328927Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.726332695Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.726754056Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes -[2025-11-18T04:07:30.726784927Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 21, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109] -[2025-11-18T04:07:30.726797125Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process -[2025-11-18T04:07:30.726800944Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.726804598Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 -[2025-11-18T04:07:30.726808736Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:07:30.726813677Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(21), LStat { file_path: TextString("./demo/sftp/std/testing/out/./65kB_random") }) -[2025-11-18T04:07:30.726819792Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./65kB_random" -[2025-11-18T04:07:30.726835911Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./65kB_random" is Attrs { size: Some(66560), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438850), ext_count: None } -[2025-11-18T04:07:30.726866185Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.726873401Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.726877014Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.726880586Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.727295586Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes -[2025-11-18T04:07:30.727327949Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 22, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109] -[2025-11-18T04:07:30.727339756Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process -[2025-11-18T04:07:30.727343616Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.727347281Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 -[2025-11-18T04:07:30.727351367Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:07:30.727355948Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(22), Stat { file_path: TextString("./demo/sftp/std/testing/out/./65kB_random") }) -[2025-11-18T04:07:30.727361867Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./65kB_random" -[2025-11-18T04:07:30.727374559Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./65kB_random" is Attrs { size: Some(66560), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438850), mtime: Some(1763438850), ext_count: None } -[2025-11-18T04:07:30.727383175Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.727407025Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.727414138Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.727417834Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.727764813Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes -[2025-11-18T04:07:30.727795509Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 23, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] -[2025-11-18T04:07:30.727806996Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process -[2025-11-18T04:07:30.727810815Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.727814500Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 -[2025-11-18T04:07:30.727818597Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 -[2025-11-18T04:07:30.727842664Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(23), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./65kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) -[2025-11-18T04:07:30.727874523Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./65kB_random", mode = SSH_FXF_READ -[2025-11-18T04:07:30.727900906Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false -[2025-11-18T04:07:30.727944983Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./65kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }) -[2025-11-18T04:07:30.727974310Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.727981587Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.727985272Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.728007435Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.728479142Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-18T04:07:30.728509478Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 24, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] -[2025-11-18T04:07:30.728518732Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process -[2025-11-18T04:07:30.728522674Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.728526339Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:07:30.728530559Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:07:30.728535521Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(24), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) -[2025-11-18T04:07:30.728561914Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 0, len = 32768 -[2025-11-18T04:07:30.728626919Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 -[2025-11-18T04:07:30.822241569Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data -[2025-11-18T04:07:30.822286676Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.822294478Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.822298544Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.822302353Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:07:30.823271801Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-18T04:07:30.823302878Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 25, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0] -[2025-11-18T04:07:30.823314571Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process -[2025-11-18T04:07:30.823318535Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:07:30.823322230Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:07:30.823327017Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:07:30.823333265Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(25), Read { handle: FileHandle(BinString(len=4)), offset: 32768, len: 32768 }) -[2025-11-18T04:07:30.823361233Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 32768, len = 32768 -[2025-11-18T04:07:30.823399700Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 -[2025-11-18T04:07:30.868777413Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data -[2025-11-18T04:07:30.868822489Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:07:30.868830405Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:07:30.868834574Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:07:30.868838310Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel \ No newline at end of file diff --git a/demo/sftp/std/testing/out/get-deadlock-client-killed.log b/demo/sftp/std/testing/out/get-deadlock-client-killed.log deleted file mode 100644 index b737b22f..00000000 --- a/demo/sftp/std/testing/out/get-deadlock-client-killed.log +++ /dev/null @@ -1,55 +0,0 @@ -Server: - -[2025-11-18T04:08:59.480314646Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:08:59.481255533Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-18T04:08:59.481288052Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 18, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0] -[2025-11-18T04:08:59.481298059Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process -[2025-11-18T04:08:59.481322710Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:08:59.481347629Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:08:59.481375365Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:08:59.481403061Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(18), Read { handle: FileHandle(BinString(len=4)), offset: 32768, len: 32768 }) -[2025-11-18T04:08:59.481434849Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 32768, len = 32768 -[2025-11-18T04:08:59.481473199Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 -[2025-11-18T04:08:59.530448787Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data -[2025-11-18T04:08:59.530485451Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:08:59.530493205Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:08:59.530497236Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:08:59.530500918Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:09:04.048434784Z WARN sunset_async::async_sunset] rx complete () -[2025-11-18T04:09:04.048557097Z INFO sunset_demo_common::server] Listening on TCP:22... - -Client: - -sftp> get ./64kB_random -debug3: Looking up ./demo/sftp/std/testing/out/./64kB_random -debug3: Sent message fd 3 T:7 I:14 -debug3: Received stat reply T:105 I:14 F:0x000f M:100644 -Fetching ./demo/sftp/std/testing/out/./64kB_random to 64kB_random -debug2: do_download: download remote "./demo/sftp/std/testing/out/./64kB_random" to local "64kB_random" -debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./64kB_random" -debug3: Sent message fd 3 T:17 I:15 -debug3: Received stat reply T:105 I:15 F:0x000f M:100644 -debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./64kB_random" -debug3: Sent remote message SSH2_FXP_OPEN I:16 P:./demo/sftp/std/testing/out/./64kB_random M:0x0001 -64kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) -debug3: Received reply T:103 I:17 R:1 -debug3: Received data 0 -> 32767 -debug3: Request range 32768 -> 65535 (0/2) -debug3: Request range 65536 -> 98303 (1/2) -debug2: channel 0: rcvd adjust 534 -debug3: Received reply T:103 I:18 R:2 -debug3: Received data 32768 -> 65535 -debug3: Finish at 98304 ( 1) -64kB_random 100% 64KB 16.0KB/s 00:04 debug3: send packet: type 1 -debug1: channel 0: free: client-session, nchannels 1 -debug3: channel 0: status: The following connections are open: - #0 client-session (t4 r0 i0/0 o0/0 e[write]/0 fd 4/5/6 sock -1 cc -1 io 0x01/0x02) - -Killed by signal 15. -DOWNLOAD Test Results: -============= -Download PASS: 512B_random -Download PASS: 16kB_random -Download PASS: 64kB_random -Download FAIL: 65kB_random -Cleaning up local files... \ No newline at end of file diff --git a/demo/sftp/std/testing/out/success-get-client.log b/demo/sftp/std/testing/out/success-get-client.log deleted file mode 100644 index 6dd21981..00000000 --- a/demo/sftp/std/testing/out/success-get-client.log +++ /dev/null @@ -1,308 +0,0 @@ -Testing Multiple GETs... -Generating random data files... -Uploading files to any@192.168.69.2... -Connected to 192.168.69.2. -sftp> put ./512B_random -Uploading ./512B_random to ./demo/sftp/std/testing/out/512B_random -512B_random 100% 512 522.1KB/s 00:00 -sftp> put ./16kB_random -Uploading ./16kB_random to ./demo/sftp/std/testing/out/16kB_random -16kB_random 100% 16KB 230.4KB/s 00:00 -sftp> put ./64kB_random -Uploading ./64kB_random to ./demo/sftp/std/testing/out/64kB_random -64kB_random 100% 64KB 214.0KB/s 00:00 -sftp> put ./65kB_random -Uploading ./65kB_random to ./demo/sftp/std/testing/out/65kB_random -65kB_random 100% 65KB 223.5KB/s 00:00 -sftp> -sftp> bye -client_loop: send disconnect: Broken pipe -UPLOAD Test Results: -============= -Upload PASS: 512B_random -Upload PASS: 16kB_random -Upload PASS: 64kB_random -Upload PASS: 65kB_random -Cleaning up original files... -OpenSSH_8.9p1 Ubuntu-3ubuntu0.13, OpenSSL 3.0.2 15 Mar 2022 -debug1: Reading configuration data /home/jubeor/.ssh/config -debug1: Reading configuration data /etc/ssh/ssh_config -debug1: /etc/ssh/ssh_config line 19: include /etc/ssh/ssh_config.d/*.conf matched no files -debug1: /etc/ssh/ssh_config line 21: Applying options for * -debug2: resolve_canonicalize: hostname 192.168.69.2 is address -debug3: ssh_connect_direct: entering -debug1: Connecting to 192.168.69.2 [192.168.69.2] port 22. -debug3: set_sock_tos: set socket 3 IP_TOS 0x10 -debug1: Connection established. -debug1: identity file /home/jubeor/.ssh/id_rsa type 0 -debug1: identity file /home/jubeor/.ssh/id_rsa-cert type -1 -debug1: identity file /home/jubeor/.ssh/id_ecdsa type -1 -debug1: identity file /home/jubeor/.ssh/id_ecdsa-cert type -1 -debug1: identity file /home/jubeor/.ssh/id_ecdsa_sk type -1 -debug1: identity file /home/jubeor/.ssh/id_ecdsa_sk-cert type -1 -debug1: identity file /home/jubeor/.ssh/id_ed25519 type -1 -debug1: identity file /home/jubeor/.ssh/id_ed25519-cert type -1 -debug1: identity file /home/jubeor/.ssh/id_ed25519_sk type -1 -debug1: identity file /home/jubeor/.ssh/id_ed25519_sk-cert type -1 -debug1: identity file /home/jubeor/.ssh/id_xmss type -1 -debug1: identity file /home/jubeor/.ssh/id_xmss-cert type -1 -debug1: identity file /home/jubeor/.ssh/id_dsa type -1 -debug1: identity file /home/jubeor/.ssh/id_dsa-cert type -1 -debug1: Local version string SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.13 -debug1: Remote protocol version 2.0, remote software version Sunset-1 -debug1: compat_banner: no match: Sunset-1 -debug2: fd 3 setting O_NONBLOCK -debug1: Authenticating to 192.168.69.2:22 as 'any' -debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory -debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory -debug3: order_hostkeyalgs: no algorithms matched; accept original -debug3: send packet: type 20 -debug1: SSH2_MSG_KEXINIT sent -debug3: receive packet: type 20 -debug1: SSH2_MSG_KEXINIT received -debug2: local client KEXINIT proposal -debug2: KEX algorithms: curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,sntrup761x25519-sha512@openssh.com,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,ext-info-c,kex-strict-c-v00@openssh.com -debug2: host key algorithms: ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,sk-ecdsa-sha2-nistp256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256 -debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com -debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com -debug2: MACs ctos: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 -debug2: MACs stoc: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 -debug2: compression ctos: none,zlib@openssh.com,zlib -debug2: compression stoc: none,zlib@openssh.com,zlib -debug2: languages ctos: -debug2: languages stoc: -debug2: first_kex_follows 0 -debug2: reserved 0 -debug2: peer server KEXINIT proposal -debug2: KEX algorithms: mlkem768x25519-sha256,curve25519-sha256,curve25519-sha256@libssh.org,kex-strict-s-v00@openssh.com,kexguess2@matt.ucc.asn.au -debug2: host key algorithms: ssh-ed25519,rsa-sha2-256 -debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes256-ctr -debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes256-ctr -debug2: MACs ctos: hmac-sha2-256 -debug2: MACs stoc: hmac-sha2-256 -debug2: compression ctos: none -debug2: compression stoc: none -debug2: languages ctos: -debug2: languages stoc: -debug2: first_kex_follows 0 -debug2: reserved 0 -debug3: kex_choose_conf: will use strict KEX ordering -debug1: kex: algorithm: curve25519-sha256 -debug1: kex: host key algorithm: ssh-ed25519 -debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: compression: none -debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: compression: none -debug3: send packet: type 30 -debug1: expecting SSH2_MSG_KEX_ECDH_REPLY -debug3: receive packet: type 31 -debug1: SSH2_MSG_KEX_ECDH_REPLY received -debug1: Server host key: ssh-ed25519 SHA256:624v6xNDiTpLgI28RnlKgSBDHRrekRUjQFY9rgia+fg -debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory -debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory -Warning: Permanently added '192.168.69.2' (ED25519) to the list of known hosts. -debug3: send packet: type 21 -debug1: ssh_packet_send2_wrapped: resetting send seqnr 3 -debug2: ssh_set_newkeys: mode 1 -debug1: rekey out after 134217728 blocks -debug1: SSH2_MSG_NEWKEYS sent -debug1: expecting SSH2_MSG_NEWKEYS -debug3: receive packet: type 21 -debug1: ssh_packet_read_poll2: resetting read seqnr 3 -debug1: SSH2_MSG_NEWKEYS received -debug2: ssh_set_newkeys: mode 0 -debug1: rekey in after 134217728 blocks -debug1: Will attempt key: /home/jubeor/.ssh/id_rsa RSA SHA256:Wc43s4t4VZ5DkRa7At3RMd9E0u/UH4CV75mWxI0G8A4 -debug1: Will attempt key: /home/jubeor/.ssh/id_ecdsa -debug1: Will attempt key: /home/jubeor/.ssh/id_ecdsa_sk -debug1: Will attempt key: /home/jubeor/.ssh/id_ed25519 -debug1: Will attempt key: /home/jubeor/.ssh/id_ed25519_sk -debug1: Will attempt key: /home/jubeor/.ssh/id_xmss -debug1: Will attempt key: /home/jubeor/.ssh/id_dsa -debug2: pubkey_prepare: done -debug3: send packet: type 5 -debug3: receive packet: type 7 -debug1: SSH2_MSG_EXT_INFO received -debug1: kex_input_ext_info: server-sig-algs= -debug3: receive packet: type 6 -debug2: service_accept: ssh-userauth -debug1: SSH2_MSG_SERVICE_ACCEPT received -debug3: send packet: type 50 -debug3: receive packet: type 52 -Authenticated to 192.168.69.2 ([192.168.69.2]:22) using "none". -debug1: channel 0: new [client-session] -debug3: ssh_session2_open: channel_new: 0 -debug2: channel 0: send open -debug3: send packet: type 90 -debug1: Entering interactive session. -debug1: pledge: network -debug3: receive packet: type 91 -debug2: channel_input_open_confirmation: channel 0: callback start -debug2: fd 3 setting TCP_NODELAY -debug3: set_sock_tos: set socket 3 IP_TOS 0x08 -debug2: client_session2_setup: id 0 -debug1: Sending environment. -debug3: Ignored env SHELL -debug3: Ignored env USER_ZDOTDIR -debug3: Ignored env COLORTERM -debug3: Ignored env WSL2_GUI_APPS_ENABLED -debug3: Ignored env TERM_PROGRAM_VERSION -debug3: Ignored env WSL_DISTRO_NAME -debug3: Ignored env NAME -debug3: Ignored env PWD -debug3: Ignored env NIX_PROFILES -debug3: Ignored env LOGNAME -debug3: Ignored env VSCODE_GIT_ASKPASS_NODE -debug3: Ignored env VSCODE_INJECTION -debug3: Ignored env HOME -debug1: channel 0: setting env LANG = "C.UTF-8" -debug2: channel 0: request env confirm 0 -debug3: send packet: type 98 -debug3: Ignored env WSL_INTEROP -debug3: Ignored env LS_COLORS -debug3: Ignored env WAYLAND_DISPLAY -debug3: Ignored env NIX_SSL_CERT_FILE -debug3: Ignored env GIT_ASKPASS -debug3: Ignored env VSCODE_GIT_ASKPASS_EXTRA_ARGS -debug3: Ignored env VSCODE_PYTHON_AUTOACTIVATE_GUARD -debug3: Ignored env TERM -debug3: Ignored env ZDOTDIR -debug3: Ignored env USER -debug3: Ignored env VSCODE_GIT_IPC_HANDLE -debug3: Ignored env DISPLAY -debug3: Ignored env SHLVL -debug3: Ignored env XDG_RUNTIME_DIR -debug3: Ignored env WSLENV -debug3: Ignored env VSCODE_GIT_ASKPASS_MAIN -debug3: Ignored env XDG_DATA_DIRS -debug3: Ignored env PATH -debug3: Ignored env DBUS_SESSION_BUS_ADDRESS -debug3: Ignored env HOSTTYPE -debug3: Ignored env PULSE_SERVER -debug3: Ignored env OLDPWD -debug3: Ignored env TERM_PROGRAM -debug3: Ignored env VSCODE_IPC_HOOK_CLI -debug3: Ignored env _ -debug1: Sending subsystem: sftp -debug2: channel 0: request subsystem confirm 1 -debug3: send packet: type 98 -debug2: channel_input_open_confirmation: channel 0: callback done -debug2: channel 0: open confirm rwindow 1000 rmax 1000 -debug3: receive packet: type 99 -debug2: channel_input_status_confirm: type 99 id 0 -debug2: subsystem request accepted on channel 0 -debug2: Remote version: 3 -Connected to 192.168.69.2. -debug2: Sending SSH2_FXP_REALPATH "." -debug3: Sent message fd 3 T:16 I:1 -debug3: SSH2_FXP_REALPATH . -> ./demo/sftp/std/testing/out/ -sftp> get ./512B_random -debug3: Looking up ./demo/sftp/std/testing/out/./512B_random -debug3: Sent message fd 3 T:7 I:2 -debug3: Received stat reply T:105 I:2 F:0x000f M:100644 -Fetching ./demo/sftp/std/testing/out/./512B_random to 512B_random -debug2: do_download: download remote "./demo/sftp/std/testing/out/./512B_random" to local "512B_random" -debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./512B_random" -debug3: Sent message fd 3 T:17 I:3 -debug3: Received stat reply T:105 I:3 F:0x000f M:100644 -debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./512B_random" -debug3: Sent remote message SSH2_FXP_OPEN I:4 P:./demo/sftp/std/testing/out/./512B_random M:0x0001 -512B_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) -debug3: Received reply T:103 I:5 R:1 -debug3: Received data 0 -> 511 -debug3: Short data block, re-requesting 512 -> 32767 ( 1) -debug3: Finish at 32768 ( 1) -debug3: Received reply T:101 I:6 R:1 -512B_random 100% 512 10.8KB/s 00:00 -debug3: Sent message SSH2_FXP_CLOSE I:7 -debug3: SSH2_FXP_STATUS 0 -sftp> get ./16kB_random -debug3: Looking up ./demo/sftp/std/testing/out/./16kB_random -debug3: Sent message fd 3 T:7 I:8 -debug3: Received stat reply T:105 I:8 F:0x000f M:100644 -Fetching ./demo/sftp/std/testing/out/./16kB_random to 16kB_random -debug2: do_download: download remote "./demo/sftp/std/testing/out/./16kB_random" to local "16kB_random" -debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./16kB_random" -debug3: Sent message fd 3 T:17 I:9 -debug2: channel 0: rcvd adjust 516 -debug3: Received stat reply T:105 I:9 F:0x000f M:100644 -debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./16kB_random" -debug3: Sent remote message SSH2_FXP_OPEN I:10 P:./demo/sftp/std/testing/out/./16kB_random M:0x0001 -16kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) -debug3: Received reply T:103 I:11 R:1 -debug3: Received data 0 -> 16383 -debug3: Short data block, re-requesting 16384 -> 32767 ( 1) -debug3: Finish at 32768 ( 1) -debug3: Received reply T:101 I:12 R:1 -16kB_random 100% 16KB 333.6KB/s 00:00 -debug3: Sent message SSH2_FXP_CLOSE I:13 -debug3: SSH2_FXP_STATUS 0 -sftp> get ./64kB_random -debug3: Looking up ./demo/sftp/std/testing/out/./64kB_random -debug3: Sent message fd 3 T:7 I:14 -debug3: Received stat reply T:105 I:14 F:0x000f M:100644 -Fetching ./demo/sftp/std/testing/out/./64kB_random to 64kB_random -debug2: do_download: download remote "./demo/sftp/std/testing/out/./64kB_random" to local "64kB_random" -debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./64kB_random" -debug3: Sent message fd 3 T:17 I:15 -debug3: Received stat reply T:105 I:15 F:0x000f M:100644 -debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./64kB_random" -debug3: Sent remote message SSH2_FXP_OPEN I:16 P:./demo/sftp/std/testing/out/./64kB_random M:0x0001 -64kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) -debug3: Received reply T:103 I:17 R:1 -debug3: Received data 0 -> 32767 -debug3: Request range 32768 -> 65535 (0/2) -debug3: Request range 65536 -> 98303 (1/2) -debug2: channel 0: rcvd adjust 520 -debug3: Received reply T:103 I:18 R:2 -debug3: Received data 32768 -> 65535 -debug3: Finish at 98304 ( 1) -debug3: Received reply T:101 I:19 R:1 -64kB_random 100% 64KB 623.4KB/s 00:00 -debug3: Sent message SSH2_FXP_CLOSE I:20 -debug3: SSH2_FXP_STATUS 0 -sftp> get ./65kB_random -debug3: Looking up ./demo/sftp/std/testing/out/./65kB_random -debug3: Sent message fd 3 T:7 I:21 -debug3: Received stat reply T:105 I:21 F:0x000f M:100644 -Fetching ./demo/sftp/std/testing/out/./65kB_random to 65kB_random -debug2: do_download: download remote "./demo/sftp/std/testing/out/./65kB_random" to local "65kB_random" -debug2: Sending SSH2_FXP_STAT "./demo/sftp/std/testing/out/./65kB_random" -debug3: Sent message fd 3 T:17 I:22 -debug3: Received stat reply T:105 I:22 F:0x000f M:100644 -debug2: Sending SSH2_FXP_OPEN "./demo/sftp/std/testing/out/./65kB_random" -debug3: Sent remote message SSH2_FXP_OPEN I:23 P:./demo/sftp/std/testing/out/./65kB_random M:0x0001 -65kB_random 0% 0 0.0KB/s --:-- ETAdebug3: Request range 0 -> 32767 (0/1) -debug2: channel 0: window 1997312 sent adjust 99840 -debug3: Received reply T:103 I:24 R:1 -debug3: Received data 0 -> 32767 -debug3: Request range 32768 -> 65535 (0/2) -debug3: Request range 65536 -> 98303 (1/2) -debug3: Received reply T:103 I:25 R:2 -debug3: Received data 32768 -> 65535 -debug3: Finish at 98304 ( 1) -debug3: Received reply T:103 I:26 R:1 -debug3: Received data 65536 -> 66559 -debug3: Short data block, re-requesting 66560 -> 98303 ( 1) -debug3: Finish at 98304 ( 1) -debug3: Received reply T:101 I:27 R:1 -65kB_random 100% 65KB 87.8KB/s 00:00 -debug3: Sent message SSH2_FXP_CLOSE I:28 -debug3: SSH2_FXP_STATUS 0 -sftp> -sftp> bye -debug2: channel 0: read failed rfd 4 maxlen 946: Broken pipe -debug2: channel 0: read failed -debug2: chan_shutdown_read: channel 0: (i0 o0 sock -1 wfd 4 efd 6 [write]) -debug2: channel 0: input open -> drain -debug2: channel 0: ibuf empty -debug2: channel 0: send eof -debug3: send packet: type 96 -debug2: channel 0: input drain -> closed -debug3: send packet: type 1 -client_loop: send disconnect: Broken pipe -DOWNLOAD Test Results: -============= -Download PASS: 512B_random -Download PASS: 16kB_random -Download PASS: 64kB_random -Download PASS: 65kB_random -Cleaning up local files... \ No newline at end of file diff --git a/demo/sftp/std/testing/out/success-get-server.log b/demo/sftp/std/testing/out/success-get-server.log deleted file mode 100644 index a78cdebb..00000000 --- a/demo/sftp/std/testing/out/success-get-server.log +++ /dev/null @@ -1,351 +0,0 @@ -[2025-11-18T04:00:55.665183905Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:00:55.665245066Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(2), LStat { file_path: TextString("./demo/sftp/std/testing/out/./512B_random") }) -[2025-11-18T04:00:55.665306824Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./512B_random" -[2025-11-18T04:00:55.665390942Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./512B_random" is Attrs { size: Some(512), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438454), ext_count: None } -[2025-11-18T04:00:55.665420750Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.665429106Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.665497840Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.665556398Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.666039369Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes -[2025-11-18T04:00:55.666069600Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 3, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, 109] -[2025-11-18T04:00:55.666080764Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process -[2025-11-18T04:00:55.666084654Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.666088265Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 -[2025-11-18T04:00:55.666092392Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:00:55.666118188Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(3), Stat { file_path: TextString("./demo/sftp/std/testing/out/./512B_random") }) -[2025-11-18T04:00:55.666146165Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./512B_random" -[2025-11-18T04:00:55.666184638Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./512B_random" is Attrs { size: Some(512), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438454), ext_count: None } -[2025-11-18T04:00:55.666220106Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.666227360Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.666231188Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.666234727Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.666614742Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes -[2025-11-18T04:00:55.666676839Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 4, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 53, 49, 50, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] -[2025-11-18T04:00:55.666693899Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process -[2025-11-18T04:00:55.666721753Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.666728760Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 -[2025-11-18T04:00:55.666734903Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 -[2025-11-18T04:00:55.666740110Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(4), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./512B_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) -[2025-11-18T04:00:55.666752653Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./512B_random", mode = SSH_FXF_READ -[2025-11-18T04:00:55.666757016Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false -[2025-11-18T04:00:55.666781248Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./512B_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }) -[2025-11-18T04:00:55.666806961Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.666833560Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.666859870Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.666886644Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.667464094Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-18T04:00:55.667505150Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 5, 0, 0, 0, 4, 55, 15, 63, 111, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] -[2025-11-18T04:00:55.667515666Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process -[2025-11-18T04:00:55.667519565Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.667523198Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:00:55.667527684Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:00:55.667556248Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(5), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) -[2025-11-18T04:00:55.667586365Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }, filepath = "./demo/sftp/std/testing/out/./512B_random", offset = 0, len = 32768 -[2025-11-18T04:00:55.667608467Z WARN sunset_demo_sftp_std::demosftpserver] Read operation: length + offset > file length. Clipping ( 32768 + 0 > 512) -[2025-11-18T04:00:55.667633306Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 512 -[2025-11-18T04:00:55.667666531Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data -[2025-11-18T04:00:55.667692091Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.667700600Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.667704438Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.667729555Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.713066511Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-18T04:00:55.713107103Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 6, 0, 0, 0, 4, 55, 15, 63, 111, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 126, 0] -[2025-11-18T04:00:55.713116827Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process -[2025-11-18T04:00:55.713120696Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.713124369Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:00:55.713129041Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:00:55.713134649Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(6), Read { handle: FileHandle(BinString(len=4)), offset: 512, len: 32256 }) -[2025-11-18T04:00:55.713142644Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [55, 15, 63, 111] }, filepath = "./demo/sftp/std/testing/out/./512B_random", offset = 512, len = 32256 -[2025-11-18T04:00:55.713156751Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF -[2025-11-18T04:00:55.713163511Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.713189492Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.713196540Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.713200728Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.713610407Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes -[2025-11-18T04:00:55.713647800Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 7, 0, 0, 0, 4, 55, 15, 63, 111] -[2025-11-18T04:00:55.713656525Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process -[2025-11-18T04:00:55.713660343Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.713663954Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 -[2025-11-18T04:00:55.713668225Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 -[2025-11-18T04:00:55.713672875Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(7), Close { handle: FileHandle(BinString(len=4)) }) -[2025-11-18T04:00:55.713678710Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./512B_random" was successful -[2025-11-18T04:00:55.713691067Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.713694741Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.713698280Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.713701769Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.714137882Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes -[2025-11-18T04:00:55.714168771Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 8, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109] -[2025-11-18T04:00:55.714181489Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process -[2025-11-18T04:00:55.714185317Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.714188939Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 -[2025-11-18T04:00:55.714193034Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:00:55.714216906Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(8), LStat { file_path: TextString("./demo/sftp/std/testing/out/./16kB_random") }) -[2025-11-18T04:00:55.714246807Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./16kB_random" -[2025-11-18T04:00:55.714286124Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./16kB_random" is Attrs { size: Some(16384), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438454), ext_count: None } -[2025-11-18T04:00:55.714319174Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.714326356Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.714349487Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.714374768Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.714840299Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes -[2025-11-18T04:00:55.714870345Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 9, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109] -[2025-11-18T04:00:55.714881622Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process -[2025-11-18T04:00:55.714885419Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.714889072Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 -[2025-11-18T04:00:55.714893177Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:00:55.714918891Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(9), Stat { file_path: TextString("./demo/sftp/std/testing/out/./16kB_random") }) -[2025-11-18T04:00:55.714946879Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./16kB_random" -[2025-11-18T04:00:55.714985578Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./16kB_random" is Attrs { size: Some(16384), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438454), ext_count: None } -[2025-11-18T04:00:55.715021828Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.715028969Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.715032828Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.715036357Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.715466142Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes -[2025-11-18T04:00:55.715495179Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 10, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 49, 54, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] -[2025-11-18T04:00:55.715506652Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process -[2025-11-18T04:00:55.715510500Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.715514122Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 -[2025-11-18T04:00:55.715518218Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 -[2025-11-18T04:00:55.715543684Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(10), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./16kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) -[2025-11-18T04:00:55.715573792Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./16kB_random", mode = SSH_FXF_READ -[2025-11-18T04:00:55.715601357Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false -[2025-11-18T04:00:55.715645088Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./16kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }) -[2025-11-18T04:00:55.715675926Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.715683221Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.715706260Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.715731798Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.716266753Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-18T04:00:55.716297673Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 11, 0, 0, 0, 4, 204, 179, 134, 195, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] -[2025-11-18T04:00:55.716307592Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process -[2025-11-18T04:00:55.716311482Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.716315124Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:00:55.716338348Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:00:55.716366994Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(11), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) -[2025-11-18T04:00:55.716379578Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }, filepath = "./demo/sftp/std/testing/out/./16kB_random", offset = 0, len = 32768 -[2025-11-18T04:00:55.716412001Z WARN sunset_demo_sftp_std::demosftpserver] Read operation: length + offset > file length. Clipping ( 32768 + 0 > 16384) -[2025-11-18T04:00:55.716438867Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 16384 -[2025-11-18T04:00:55.762253804Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data -[2025-11-18T04:00:55.762324452Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.762334053Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.762339033Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.762343859Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.763463786Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-18T04:00:55.763502124Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 12, 0, 0, 0, 4, 204, 179, 134, 195, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 64, 0] -[2025-11-18T04:00:55.763512085Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process -[2025-11-18T04:00:55.763515943Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.763519689Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:00:55.763524350Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:00:55.763551062Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(12), Read { handle: FileHandle(BinString(len=4)), offset: 16384, len: 16384 }) -[2025-11-18T04:00:55.763582548Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [204, 179, 134, 195] }, filepath = "./demo/sftp/std/testing/out/./16kB_random", offset = 16384, len = 16384 -[2025-11-18T04:00:55.763619096Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF -[2025-11-18T04:00:55.763630240Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.763652990Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.763678303Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.763705405Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.764155975Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes -[2025-11-18T04:00:55.764185908Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 13, 0, 0, 0, 4, 204, 179, 134, 195] -[2025-11-18T04:00:55.764194664Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process -[2025-11-18T04:00:55.764198554Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.764202165Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 -[2025-11-18T04:00:55.764206333Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 -[2025-11-18T04:00:55.764231398Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(13), Close { handle: FileHandle(BinString(len=4)) }) -[2025-11-18T04:00:55.764260054Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./16kB_random" was successful -[2025-11-18T04:00:55.764296274Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.764304793Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.764308498Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.764331608Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.764773803Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes -[2025-11-18T04:00:55.764804712Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 14, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109] -[2025-11-18T04:00:55.764816370Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process -[2025-11-18T04:00:55.764820229Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.764823841Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 -[2025-11-18T04:00:55.764827926Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:00:55.764852549Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(14), LStat { file_path: TextString("./demo/sftp/std/testing/out/./64kB_random") }) -[2025-11-18T04:00:55.764883345Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./64kB_random" -[2025-11-18T04:00:55.764924143Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./64kB_random" is Attrs { size: Some(65536), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438455), ext_count: None } -[2025-11-18T04:00:55.764963604Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.764971959Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.764995049Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.765003425Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.765431183Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes -[2025-11-18T04:00:55.765462833Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 15, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109] -[2025-11-18T04:00:55.765475027Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process -[2025-11-18T04:00:55.765479616Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.765483248Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 -[2025-11-18T04:00:55.765508077Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:00:55.765518377Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(15), Stat { file_path: TextString("./demo/sftp/std/testing/out/./64kB_random") }) -[2025-11-18T04:00:55.765524664Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./64kB_random" -[2025-11-18T04:00:55.765539511Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./64kB_random" is Attrs { size: Some(65536), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438455), ext_count: None } -[2025-11-18T04:00:55.765567746Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.765574887Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.765578529Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.765584693Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.766011422Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes -[2025-11-18T04:00:55.766042939Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 16, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 52, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] -[2025-11-18T04:00:55.766054566Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process -[2025-11-18T04:00:55.766058425Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.766062047Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 -[2025-11-18T04:00:55.766066132Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 -[2025-11-18T04:00:55.766090518Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(16), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./64kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) -[2025-11-18T04:00:55.766123105Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./64kB_random", mode = SSH_FXF_READ -[2025-11-18T04:00:55.766149220Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false -[2025-11-18T04:00:55.766195904Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./64kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }) -[2025-11-18T04:00:55.766227102Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.766235097Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.766238842Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.766260810Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.766789201Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-18T04:00:55.766822477Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 17, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] -[2025-11-18T04:00:55.766831871Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process -[2025-11-18T04:00:55.766835771Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.766839445Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:00:55.766843509Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:00:55.766849148Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(17), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) -[2025-11-18T04:00:55.766857575Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 0, len = 32768 -[2025-11-18T04:00:55.766872742Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 -[2025-11-18T04:00:55.818563436Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data -[2025-11-18T04:00:55.818607218Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.818615470Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.818619627Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.818623455Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.819563191Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 58 bytes -[2025-11-18T04:00:55.819598515Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 18, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 25, 5, 0, 0, 0, 19, 0, 0, 0, 4, 195, 17, 206, 102, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 128, 0] -[2025-11-18T04:00:55.819610441Z TRACE sunset_sftp::sftphandler::sftphandler] Received 58 bytes to process -[2025-11-18T04:00:55.819614320Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.819618065Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 58 -[2025-11-18T04:00:55.819622459Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 58 -[2025-11-18T04:00:55.819628046Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(18), Read { handle: FileHandle(BinString(len=4)), offset: 32768, len: 32768 }) -[2025-11-18T04:00:55.819634909Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 32768, len = 32768 -[2025-11-18T04:00:55.819649798Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 -[2025-11-18T04:00:55.868682151Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data -[2025-11-18T04:00:55.868726129Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 29 bytes -[2025-11-18T04:00:55.868734330Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 29 -[2025-11-18T04:00:55.868738569Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:00:55.868743220Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:00:55.868749167Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(19), Read { handle: FileHandle(BinString(len=4)), offset: 65536, len: 32768 }) -[2025-11-18T04:00:55.868756535Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [195, 17, 206, 102] }, filepath = "./demo/sftp/std/testing/out/./64kB_random", offset = 65536, len = 32768 -[2025-11-18T04:00:55.868770940Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF -[2025-11-18T04:00:55.868777412Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.868781096Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.868784635Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.868788329Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.869538429Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes -[2025-11-18T04:00:55.869571994Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 20, 0, 0, 0, 4, 195, 17, 206, 102] -[2025-11-18T04:00:55.869581419Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process -[2025-11-18T04:00:55.869585236Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.869588920Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 -[2025-11-18T04:00:55.869593468Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 -[2025-11-18T04:00:55.869598643Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(20), Close { handle: FileHandle(BinString(len=4)) }) -[2025-11-18T04:00:55.869605013Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./64kB_random" was successful -[2025-11-18T04:00:55.869617021Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.869620848Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.869624378Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.869627928Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.870109675Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes -[2025-11-18T04:00:55.870147304Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 7, 0, 0, 0, 21, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109] -[2025-11-18T04:00:55.870158911Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process -[2025-11-18T04:00:55.870162780Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.870166381Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 -[2025-11-18T04:00:55.870170487Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:00:55.870174983Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: LStat(ReqId(21), LStat { file_path: TextString("./demo/sftp/std/testing/out/./65kB_random") }) -[2025-11-18T04:00:55.870181579Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./65kB_random" -[2025-11-18T04:00:55.870202528Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./65kB_random" is Attrs { size: Some(66560), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438455), ext_count: None } -[2025-11-18T04:00:55.870216543Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.870220267Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.870223797Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.870227316Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.870631336Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 54 bytes -[2025-11-18T04:00:55.870661073Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 50, 17, 0, 0, 0, 22, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109] -[2025-11-18T04:00:55.870672957Z TRACE sunset_sftp::sftphandler::sftphandler] Received 54 bytes to process -[2025-11-18T04:00:55.870718725Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.870723098Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 54 -[2025-11-18T04:00:55.870727224Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 54 -[2025-11-18T04:00:55.870752393Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Stat(ReqId(22), Stat { file_path: TextString("./demo/sftp/std/testing/out/./65kB_random") }) -[2025-11-18T04:00:55.870781152Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer ListStats: file_path = "./demo/sftp/std/testing/out/./65kB_random" -[2025-11-18T04:00:55.870823699Z DEBUG sunset_sftp::sftphandler::sftphandler] List stats for "./demo/sftp/std/testing/out/./65kB_random" is Attrs { size: Some(66560), uid: Some(1000), gid: Some(1000), permissions: Some(33188), atime: Some(1763438455), mtime: Some(1763438455), ext_count: None } -[2025-11-18T04:00:55.870854805Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.870861977Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.870884809Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.870909864Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.871287348Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 62 bytes -[2025-11-18T04:00:55.871316292Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 58, 3, 0, 0, 0, 23, 0, 0, 0, 41, 46, 47, 100, 101, 109, 111, 47, 115, 102, 116, 112, 47, 115, 116, 100, 47, 116, 101, 115, 116, 105, 110, 103, 47, 111, 117, 116, 47, 46, 47, 54, 53, 107, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, 0, 1, 0, 0, 0, 0] -[2025-11-18T04:00:55.871327775Z TRACE sunset_sftp::sftphandler::sftphandler] Received 62 bytes to process -[2025-11-18T04:00:55.871331624Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.871335235Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 62 -[2025-11-18T04:00:55.871358994Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 62 -[2025-11-18T04:00:55.871386025Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Open(ReqId(23), Open { filename: Filename(TextString("./demo/sftp/std/testing/out/./65kB_random")), pflags: SSH_FXF_READ, attrs: Attrs { size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, ext_count: None } }) -[2025-11-18T04:00:55.871401984Z DEBUG sunset_demo_sftp_std::demosftpserver] Open file: filename = "./demo/sftp/std/testing/out/./65kB_random", mode = SSH_FXF_READ -[2025-11-18T04:00:55.871407159Z DEBUG sunset_demo_sftp_std::demosftpserver] File open for read/write access: can_read=true, can_write=false -[2025-11-18T04:00:55.871429652Z DEBUG sunset_demo_sftp_std::demosftpserver] Filename ""./demo/sftp/std/testing/out/./65kB_random"" will have the obscured file handle: Ok(DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }) -[2025-11-18T04:00:55.871437802Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:55.871441475Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:55.871465748Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:55.871472755Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:55.872012803Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-18T04:00:55.872042880Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 24, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0] -[2025-11-18T04:00:55.872051955Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process -[2025-11-18T04:00:55.872055834Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:55.872059467Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:00:55.872063572Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:00:55.872068357Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(24), Read { handle: FileHandle(BinString(len=4)), offset: 0, len: 32768 }) -[2025-11-18T04:00:55.872093875Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 0, len = 32768 -[2025-11-18T04:00:55.872129796Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 -[2025-11-18T04:00:56.048175463Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data -[2025-11-18T04:00:56.048246934Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:56.048263223Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:56.048272051Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:56.048280180Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:56.128752382Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 58 bytes -[2025-11-18T04:00:56.128827475Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 25, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 25, 5, 0, 0, 0, 26, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 128, 0] -[2025-11-18T04:00:56.128947400Z TRACE sunset_sftp::sftphandler::sftphandler] Received 58 bytes to process -[2025-11-18T04:00:56.129007450Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:56.129023039Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 58 -[2025-11-18T04:00:56.129073612Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 58 -[2025-11-18T04:00:56.129134588Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(25), Read { handle: FileHandle(BinString(len=4)), offset: 32768, len: 32768 }) -[2025-11-18T04:00:56.129197118Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 32768, len = 32768 -[2025-11-18T04:00:56.129281842Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 32768 -[2025-11-18T04:00:56.571741252Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data -[2025-11-18T04:00:56.571788635Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 29 bytes -[2025-11-18T04:00:56.571796620Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 29 -[2025-11-18T04:00:56.571800725Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:00:56.571805592Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:00:56.571836934Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(26), Read { handle: FileHandle(BinString(len=4)), offset: 65536, len: 32768 }) -[2025-11-18T04:00:56.571867299Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 65536, len = 32768 -[2025-11-18T04:00:56.571887909Z WARN sunset_demo_sftp_std::demosftpserver] Read operation: length + offset > file length. Clipping ( 32768 + 65536 > 66560) -[2025-11-18T04:00:56.571912048Z DEBUG sunset_demo_sftp_std::demosftpserver] Starting reading loop: remaining = 1024 -[2025-11-18T04:00:56.571973714Z DEBUG sunset_demo_sftp_std::demosftpserver] Finished sending data -[2025-11-18T04:00:56.571998913Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:56.572006229Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:56.572010056Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:56.572013740Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:56.612043057Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-18T04:00:56.612080511Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 25, 5, 0, 0, 0, 27, 0, 0, 0, 4, 220, 94, 228, 181, 0, 0, 0, 0, 0, 1, 4, 0, 0, 0, 124, 0] -[2025-11-18T04:00:56.612090348Z TRACE sunset_sftp::sftphandler::sftphandler] Received 29 bytes to process -[2025-11-18T04:00:56.612094268Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:56.612097901Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-18T04:00:56.612102737Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-18T04:00:56.612129695Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Read(ReqId(27), Read { handle: FileHandle(BinString(len=4)), offset: 66560, len: 31744 }) -[2025-11-18T04:00:56.612159638Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Read operation: handle = DemoOpaqueFileHandle { tiny_hash: [220, 94, 228, 181] }, filepath = "./demo/sftp/std/testing/out/./65kB_random", offset = 66560, len = 31744 -[2025-11-18T04:00:56.612179991Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF -[2025-11-18T04:00:56.612188727Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:56.612192482Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:56.612213977Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:56.612221015Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:56.612667861Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes -[2025-11-18T04:00:56.612702804Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [0, 0, 0, 13, 4, 0, 0, 0, 28, 0, 0, 0, 4, 220, 94, 228, 181] -[2025-11-18T04:00:56.612711519Z TRACE sunset_sftp::sftphandler::sftphandler] Received 17 bytes to process -[2025-11-18T04:00:56.612715368Z TRACE sunset_sftp::sftphandler::sftphandler] Entering loop to process the full received buffer -[2025-11-18T04:00:56.612719288Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 -[2025-11-18T04:00:56.612723435Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 -[2025-11-18T04:00:56.612750280Z DEBUG sunset_sftp::sftphandler::sftphandler] Handling general request: Close(ReqId(28), Close { handle: FileHandle(BinString(len=4)) }) -[2025-11-18T04:00:56.612779163Z DEBUG sunset_demo_sftp_std::demosftpserver] SftpServer Close operation on file "./demo/sftp/std/testing/out/./65kB_random" was successful -[2025-11-18T04:00:56.612796758Z TRACE sunset_sftp::sftphandler::sftphandler] New buffer len 0 bytes -[2025-11-18T04:00:56.612819138Z TRACE sunset_sftp::sftphandler::sftphandler] Process checking buf len 0 -[2025-11-18T04:00:56.612828162Z TRACE sunset_sftp::sftphandler::sftphandler] Exiting process with Ok(()) -[2025-11-18T04:00:56.612832453Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP: About to read bytes from SSH Channel -[2025-11-18T04:00:56.613342157Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 0 bytes -[2025-11-18T04:00:56.613381792Z TRACE sunset_sftp::sftphandler::sftphandler] SFTP <---- received: [] -[2025-11-18T04:00:56.613390528Z DEBUG sunset_sftp::sftphandler::sftphandler] client disconnected -[2025-11-18T04:00:56.613394932Z DEBUG sunset_sftp::sftphandler::sftphandler] Processing returned: Err(ClientDisconnected) -[2025-11-18T04:00:56.613402269Z WARN sunset_demo_sftp_std] sftp_loop finished: Err(ChannelEOF) -[2025-11-18T04:00:56.613409008Z WARN sunset_demo_common::server] Ended with error ChannelEOF -[2025-11-18T04:00:56.613464932Z INFO sunset_demo_common::server] Listening on TCP:22... \ No newline at end of file From 6792cec7bfc2e02c30b50b7d6e6fb583854803f4 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 21 Nov 2025 09:59:47 +1100 Subject: [PATCH 369/393] [skip ci] Adding test_get_single.sh and simplifying test_get.sh The upload process has been removed to reduce verbosity and test time. Also keeps the test focus on dowloading items. test_get_single.sh only performs a get transaction of a long file (4MB) Tests will keep failing check files for further inspection --- demo/sftp/std/testing/test_get.sh | 36 ++++++----------- demo/sftp/std/testing/test_get_single.sh | 50 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 25 deletions(-) create mode 100755 demo/sftp/std/testing/test_get_single.sh diff --git a/demo/sftp/std/testing/test_get.sh b/demo/sftp/std/testing/test_get.sh index b46d91c4..73acd192 100755 --- a/demo/sftp/std/testing/test_get.sh +++ b/demo/sftp/std/testing/test_get.sh @@ -2,6 +2,10 @@ echo "Testing Multiple GETs..." +echo "Cleaning up previous run files" +rm -f -r ./*_random ./out/*_random + + # Set remote server details REMOTE_HOST="192.168.69.2" REMOTE_USER="any" @@ -9,7 +13,7 @@ REMOTE_USER="any" # Define test files FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random" "2048kB_random") -# Generate random data files + echo "Generating random data files..." dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null @@ -20,28 +24,13 @@ dd if=/dev/random bs=1024 count=1024 of=./1024kB_random 2>/dev/null dd if=/dev/random bs=1024 count=2048 of=./2048kB_random 2>/dev/null echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." -# Upload the files -sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF -$(printf 'put ./%s\n' "${FILES[@]}") - -bye -EOF - -echo "UPLOAD Test Results:" -echo "=============" -# Test each file +echo "Moving to the server folder..." for file in "${FILES[@]}"; do - if diff "./${file}" "./out/${file}" >/dev/null 2>&1; then - echo "Upload PASS: ${file}" - else - echo "Upload FAIL: ${file}" - fi + mv "./${file}" "./out/${file}" done -echo "Cleaning up original files..." -rm -f -r ./*_random -# Download the files +echo "Downloading files..." sftp -vvvvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'get ./%s\n' "${FILES[@]}") @@ -53,12 +42,9 @@ echo "=============" # Test each file for file in "${FILES[@]}"; do if diff "./${file}" "./out/${file}" >/dev/null 2>&1; then - echo "Download PASS: ${file}" + echo "Download PASS: ${file}. Cleaning it" + rm -f -r ./${file} ./out/${file} else - echo "Download FAIL: ${file}" + echo "Download FAIL: ${file}". Keeping for inspection fi done - - -echo "Cleaning up local files..." -rm -f -r ./*_random ./out/*_random diff --git a/demo/sftp/std/testing/test_get_single.sh b/demo/sftp/std/testing/test_get_single.sh new file mode 100755 index 00000000..182c0eec --- /dev/null +++ b/demo/sftp/std/testing/test_get_single.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +echo "Testing Single long GETs..." + +echo "Cleaning up previous run files" +rm -f -r ./*_random ./out/*_random + + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Define test files +# FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random" "2048kB_random") +FILES=("4096kB_random") + +echo "Generating random data files..." +# dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null +# dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null +# dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null +# dd if=/dev/random bs=1024 count=65 of=./65kB_random 2>/dev/null +# dd if=/dev/random bs=1024 count=256 of=./256kB_random 2>/dev/null +# dd if=/dev/random bs=1024 count=1024 of=./1024kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=4096 of=./4096kB_random 2>/dev/null +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." + +echo "Moving to the server folder..." +for file in "${FILES[@]}"; do + mv "./${file}" "./out/${file}" +done + + +echo "Downloading files..." +sftp -vvvvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +$(printf 'get ./%s\n' "${FILES[@]}") + +bye +EOF + +echo "DOWNLOAD Test Results:" +echo "=============" +# Test each file +for file in "${FILES[@]}"; do + if diff "./${file}" "./out/${file}" >/dev/null 2>&1; then + echo "Download PASS: ${file}. Cleaning it" + rm -f -r ./${file} ./out/${file} + else + echo "Download FAIL: ${file}". Keeping for inspection + fi +done From 58ddc566ac1c74a324d296751b72e3387a5944db Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 25 Nov 2025 12:26:08 +1100 Subject: [PATCH 370/393] [skip ci] Refactor packet handling: Packets will have fixed length part and extra data This normalize the way of treating packets while decoding and completing fragments. Processing long write requests is simplified with a new state `HandlerState::ProcessWriteRequest`, that keeps the progress and simply consume data for the the extra data that is no longer part of the proto definition of Write. requestholder.rs has new methods that simplify its usage and makes it generic. sftpsource.rs has been simplified most warnings have been corrected --- demo/sftp/std/src/main.rs | 4 + sftp/src/lib.rs | 1 - sftp/src/proto.rs | 18 +- sftp/src/sftperror.rs | 2 +- sftp/src/sftphandler/mod.rs | 3 +- .../sftphandler/partialwriterequesttracker.rs | 60 - sftp/src/{ => sftphandler}/requestholder.rs | 253 ++-- sftp/src/sftphandler/sftphandler.rs | 1084 ++++++++--------- sftp/src/sftpserver.rs | 2 +- sftp/src/sftpsource.rs | 144 +-- 10 files changed, 691 insertions(+), 880 deletions(-) delete mode 100644 sftp/src/sftphandler/partialwriterequesttracker.rs rename sftp/src/{ => sftphandler}/requestholder.rs (55%) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 6828c7d1..2694d25e 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -226,6 +226,10 @@ async fn main(spawner: Spawner) { log::LevelFilter::Debug, ) // .filter_module( + // "sunset_sftp::requestholder", + // log::LevelFilter::Debug, + // ) + // .filter_module( // "sunset_demo_sftp_std::demosftpserver", // log::LevelFilter::Debug, // ) diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 4e514b6d..4e3b2c0e 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -53,7 +53,6 @@ mod opaquefilehandle; mod proto; -mod requestholder; mod sftperror; mod sftphandler; mod sftpserver; diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 8dc52b30..6af64d76 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -41,6 +41,7 @@ pub const SFTP_FIELD_REQ_ID_LEN: usize = 4; /// SFTP SSH_FXP_WRITE Packet cannot be shorter than this (len:4+pnum:1+rid:4+hand:4+0+data:4+0 bytes = 17 bytes) [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) // pub const SFTP_MINIMUM_WRITE_PACKET_LEN: usize = 17; +#[allow(unused)] /// SFTP SSH_FXP_WRITE Packet request id field index [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) pub const SFTP_WRITE_REQID_INDEX: usize = 5; @@ -210,9 +211,16 @@ pub struct Write<'a> { /// The offset for the read operation pub offset: u64, - pub data: BinString<'a>, + pub data_len: u32, + // pub data: BinString<'a>, // TODO: Find an elegant way to process the write process } +// TODO: This cannot work because we would need a length field +// #[derive(Debug, SSHEncode, SSHDecode)] +// pub struct WriteData<'a> { +// pub data_slice: &'a [u8], +// } + /// Used for `ssh_fxp_lstat` [response](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.8). /// LSTAT does not follow symbolic links #[derive(Debug, SSHEncode, SSHDecode)] @@ -364,7 +372,7 @@ pub struct ResponseAttributes { // Requests/Responses data types -#[derive(Debug, SSHEncode, SSHDecode, Clone, Copy, PartialEq)] +#[derive(Debug, SSHEncode, SSHDecode, Clone, Copy, PartialEq, Eq)] pub struct ReqId(pub u32); /// For more information see [Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) @@ -595,7 +603,7 @@ macro_rules! sftpmessages { ) => { paste! { /// Represent a subset of the SFTP packet types defined by draft-ietf-secsh-filexfer-02 - #[derive(Debug, Clone, PartialEq, FromPrimitive, SSHEncode)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive, SSHEncode)] #[repr(u8)] #[allow(non_camel_case_types)] pub enum SftpNum { @@ -656,7 +664,7 @@ macro_rules! sftpmessages { (1..=1).contains(&(u8::from(self.clone()))) } - fn is_request(&self) -> bool { + pub(crate) fn is_request(&self) -> bool { // TODO SSH_FXP_EXTENDED (3..=20).contains(&(u8::from(self.clone()))) } @@ -862,7 +870,7 @@ macro_rules! sftpmessages { }, Err(e) => { match e { - WireError::UnknownPacket{..} if !s.packet_fits()? => Err(WireError::RanOut), + WireError::UnknownPacket{..} if !s.packet_fits() => Err(WireError::RanOut), _ => Err(e) } diff --git a/sftp/src/sftperror.rs b/sftp/src/sftperror.rs index 8fd251d9..7fbdef8c 100644 --- a/sftp/src/sftperror.rs +++ b/sftp/src/sftperror.rs @@ -1,6 +1,6 @@ use crate::protocol::StatusCode; -use crate::requestholder::RequestHolderError; +use crate::sftphandler::requestholder::RequestHolderError; use sunset::Error as SunsetError; use sunset::sshwire::WireError; diff --git a/sftp/src/sftphandler/mod.rs b/sftp/src/sftphandler/mod.rs index 6122c204..988cc09f 100644 --- a/sftp/src/sftphandler/mod.rs +++ b/sftp/src/sftphandler/mod.rs @@ -1,7 +1,6 @@ -mod partialwriterequesttracker; +pub mod requestholder; mod sftphandler; mod sftpoutputchannelhandler; -pub use partialwriterequesttracker::PartialWriteRequestTracker; pub use sftphandler::SftpHandler; pub use sftpoutputchannelhandler::SftpOutputProducer; diff --git a/sftp/src/sftphandler/partialwriterequesttracker.rs b/sftp/src/sftphandler/partialwriterequesttracker.rs deleted file mode 100644 index 38ea4f6a..00000000 --- a/sftp/src/sftphandler/partialwriterequesttracker.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::handles::OpaqueFileHandle; -use crate::proto::ReqId; -use sunset::sshwire::WireResult; - -// TODO Generalize this to allow other request types -/// Used to keep record of a long SFTP Write request that does not fit in -/// receiving buffer and requires processing in batches -#[derive(Debug)] -pub struct PartialWriteRequestTracker { - req_id: ReqId, - opaque_handle: T, - remain_data_len: u32, - remain_data_offset: u64, -} - -impl PartialWriteRequestTracker { - /// Creates a new [`PartialWriteRequestTracker`] - pub fn new( - req_id: ReqId, - opaque_handle: T, - remain_data_len: u32, - remain_data_offset: u64, - ) -> WireResult { - Ok(PartialWriteRequestTracker { - req_id, - opaque_handle: opaque_handle, - remain_data_len, - remain_data_offset, - }) - } - /// Returns the opaque file handle associated with the request - /// tracked - pub fn get_opaque_file_handle(&self) -> T { - self.opaque_handle.clone() - } - - pub fn get_remain_data_len(&self) -> u32 { - self.remain_data_len - } - - pub fn get_remain_data_offset(&self) -> u64 { - self.remain_data_offset - } - - // pub fn add_to_remain_data_offset(&mut self, add_offset: u64) { - // self.remain_data_offset += add_offset; - // } - - pub(crate) fn update_remaining_after_partial_write( - &mut self, - data_segment_len: u32, - ) -> () { - self.remain_data_offset += data_segment_len as u64; - self.remain_data_len -= data_segment_len; - } - - pub(crate) fn get_req_id(&self) -> ReqId { - self.req_id.clone() - } -} diff --git a/sftp/src/requestholder.rs b/sftp/src/sftphandler/requestholder.rs similarity index 55% rename from sftp/src/requestholder.rs rename to sftp/src/sftphandler/requestholder.rs index a33a8fcf..4fa65d4f 100644 --- a/sftp/src/requestholder.rs +++ b/sftp/src/sftphandler/requestholder.rs @@ -1,5 +1,5 @@ use crate::{ - proto::{self, SFTP_FIELD_LEN_LENGTH}, + proto::{SftpNum, SftpPacket}, sftpsource::SftpSource, }; @@ -17,9 +17,10 @@ pub enum RequestHolderError { Empty, /// There is not enough data in the slice we are trying to add. we need more data RanOut, + /// The Packet held is not a request + NotRequest, /// WireError WireError(WireError), - Bug, } impl From for RequestHolderError { @@ -51,7 +52,7 @@ pub(crate) type RequestHolderResult = Result; /// /// - `reset`: reset counters and flags to allow `try_hold` a new request /// -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub(crate) struct RequestHolder<'a> { /// The buffer used to contain the data for the request buffer: &'a mut [u8], @@ -75,11 +76,6 @@ impl<'a> RequestHolder<'a> { } } - /// Returns the maximum request size that the holder can hold. - pub(crate) fn capacity(&self) -> usize { - self.buffer.len() - SFTP_FIELD_LEN_LENGTH - } - /// Uses the internal buffer to store a copy of the provided slice /// /// The definition of `try_hold` and `try_append_slice` separately @@ -115,29 +111,29 @@ impl<'a> RequestHolder<'a> { self.appended = 0; } - /// Using the content of the `RequestHolder` tries to find a valid - /// SFTP request appending bytes from slice_in into the internal buffer to - /// form a valid request. + /// Appends a byte at a time to the internal buffer and tries to + /// decode a request /// /// Reset and increase the `appended()` counter. /// /// **Returns**: /// - /// - `Ok(())`: Full valid request + /// - `Ok(())`: A valid request is held now /// - /// - `Err(RanOut)`: Not enough bytes in the slice to complete a valid request or fill the buffer + /// - `Err(NotRequest)`: The decoded packet is not a request /// - /// - `Err(NoRoom)`: The internal buffer is full but there is not a full valid request in the buffer + /// - `Err(RanOut)`: Not enough bytes in the slice to add a single byte + /// + /// - `Err(NoRoom)`: The internal buffer is full /// /// - `Err(Empty)`: If the structure has not been loaded with `try_hold` /// - /// - `Err(Bug)`: An unexpected condition arises - pub(crate) fn try_append_for_valid_request( + pub(crate) fn try_appending_for_valid_request( &mut self, slice_in: &[u8], - ) -> RequestHolderResult<()> { + ) -> RequestHolderResult { debug!( - "try_append_for_valid_request: self = {:?}\n\ + "try_appending_for_valid_request: self = {:?}\n\ Space left = {:?}\n\ Length of slice to append from = {:?}", self, @@ -150,112 +146,70 @@ impl<'a> RequestHolder<'a> { return Err(RequestHolderError::Empty); } + self.appended = 0; // reset appended bytes counter. Try_append_slice will increase it + if self.is_full() { error!("Request Holder is full"); return Err(RequestHolderError::NoRoom); } - self.appended = 0; // reset appended bytes counter - - // If we will not be able to read the SFTP packet ID we clearly need more data - if self.buffer_fill_index + slice_in.len() < proto::SFTP_FIELD_ID_INDEX { - self.try_append_slice(&slice_in)?; - error!( - "[Buffer fill index = {:?}] + [slice.len = {:?}] = {:?} < SFTP field id index = {:?}", - self.buffer_fill_index, - slice_in.len(), - self.buffer_fill_index + slice_in.len(), - proto::SFTP_FIELD_ID_INDEX - ); - return Err(RequestHolderError::RanOut); + if let Some(request) = self.valid_request() { + debug!("The request holder already contained a valid request"); + return Ok(request.sftp_num()); } - let complete_to_id_index = (proto::SFTP_FIELD_ID_INDEX + 1) - .checked_sub(self.buffer_fill_index) - .unwrap_or(0); - - if complete_to_id_index > 0 { + let mut slice = slice_in; + loop { debug!( - "The held fragment len = {:?}, is insufficient to peak \ - the length and type. Will append {:?} to reach the \ - id field index: {:?}", - self.buffer_fill_index, - complete_to_id_index, - proto::SFTP_FIELD_ID_INDEX + "try_appending_for_valid_request: Slice length {:?}", + slice.len() ); - if complete_to_id_index > slice_in.len() { - self.try_append_slice(&slice_in)?; - error!( - "The slice to include to the held fragment is too \ - short to complete to id index. More data is required." - ); - return Err(RequestHolderError::RanOut); + if slice.len() > 0 { + self.try_append_slice(&[slice[0]])?; + slice = &slice[1..]; + let mut source = SftpSource::new(self.try_get_ref()?); + if let Ok(pt) = source.peak_packet_type() { + if !pt.is_request() { + error!("The request candidate is not a request {pt:?}"); + return Err(RequestHolderError::NotRequest); + } + } else { + continue; + }; + match SftpPacket::decode_request(&mut source) { + Ok(request) => { + debug!("Request is {:?}", request); + return Ok(request.sftp_num()); + } + Err(WireError::RanOut) => { + if slice.len() == 0 { + return Err(RequestHolderError::RanOut); + } + } + Err(WireError::NoRoom) => { + return Err(RequestHolderError::NoRoom); + } + Err(WireError::PacketWrong) => { + return Err(RequestHolderError::NotRequest); + } + Err(e) => return Err(RequestHolderError::WireError(e)), + } } else { - self.try_append_slice(&slice_in[..complete_to_id_index])?; - }; - } - - let (packet_len, packet_type) = { - let temp_source = SftpSource::new(self.try_get_ref()?); - let packet_len = temp_source.peak_packet_len()? as usize; - let packet_type = temp_source.peak_packet_type()?; - (packet_len, packet_type) - }; - debug!("Request len = {:?}, type = {:?}", packet_len, packet_type); - - let remaining_packet_len = - packet_len - (self.buffer_fill_index - proto::SFTP_FIELD_LEN_LENGTH); - // The packet len does not include the packet len field itself (4 bytes) - // https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-3 - debug!( - "[Total Packet len = {:?}] = [Packet len copied so far = {:}] \ - - [SFTP Field len length = {:?}] + [Remaining packet len = {:?}]", - packet_len, - self.buffer_fill_index, - proto::SFTP_FIELD_LEN_LENGTH, - remaining_packet_len, - ); - assert_eq!( - packet_len, - self.buffer_fill_index - proto::SFTP_FIELD_LEN_LENGTH - + remaining_packet_len - ); - if remaining_packet_len <= self.remaining_len() { - // The remaining bytes would fill in the buffer - - if (slice_in.len()) < (remaining_packet_len + self.appended()) { - // the slice_in does not contain all the remaining bytes - // We added them an request more - self.try_append_slice(&slice_in[self.appended()..])?; return Err(RequestHolderError::RanOut); - } else { - self.try_append_slice( - &slice_in[self.appended()..remaining_packet_len], - )?; - return Ok(()); } - } else { - // the remaining packet bytes are more than we can fit in the buffer - // But they may not fit in the slice neither - - let start = self.appended(); - let end = self.remaining_len().min(slice_in.len() - self.appended()); + } + } - debug!( - "Will finally take the range: [{:?}..{:?}] from the slice [0..{:?}]", - start, - end, - slice_in.len() - ); - self.try_append_slice( - &slice_in[self.appended() - ..self.remaining_len().min(slice_in.len() - self.appended())], - )?; - if self.is_full() { - return Err(RequestHolderError::NoRoom); - } else { - return Err(RequestHolderError::RanOut); // More bytes are needed to complete the Write request + pub(crate) fn valid_request(&self) -> Option> { + if !self.busy { + return None; + } + let mut source = SftpSource::new(self.try_get_ref().unwrap_or(&[0])); + match SftpPacket::decode_request(&mut source) { + Ok(request) => { + return Some(request); } + Err(..) => return None, } } @@ -282,7 +236,11 @@ impl<'a> RequestHolder<'a> { self.busy } - /// Returns the bytes appened in the last call to `try_append_for_valid_request` + /// Returns the bytes appened in the last call to + /// [`RequestHolder::try_append_for_valid_request`] or + /// [`RequestHolder::try_append_for_valid_header`] or + /// [`RequestHolder::try_append_slice`] or + /// [`RequestHolder::try_appending_single_byte`] pub(crate) fn appended(&self) -> usize { self.appended } @@ -333,3 +291,76 @@ impl<'a> RequestHolder<'a> { self.buffer.len() - self.buffer_fill_index } } + +#[cfg(test)] +mod local_test { + use super::*; + // use crate::requestholder::RequestHolder; + + #[cfg(test)] + extern crate std; + #[cfg(test)] + use std::println; + + fn get_buffer_with_valid_request() -> [u8; 85] { + [ + 0, 0, 128, 25, 6, 0, 0, 0, 23, 0, 0, 0, 4, 249, 67, 81, 122, 0, 0, 0, 0, + 0, 9, 128, 0, 0, 0, 128, 0, 116, 101, 115, 116, 105, 110, 103, 47, 111, + 117, 116, 47, 49, 48, 48, 77, 66, 95, 114, 97, 110, 100, 111, 109, 0, 0, + 0, 26, 0, 0, 0, 4, 0, 0, 1, 164, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ] + } + #[test] + fn valid_request_uses_filled_data() { + let mut clean_buffer = [0u8; 256]; + let buff_data = get_buffer_with_valid_request(); + + let mut rh = RequestHolder::new(&mut clean_buffer); + rh.try_hold(&buff_data).unwrap(); + assert!(rh.valid_request().is_some()); + + rh.reset(); + assert!(rh.valid_request().is_none()); + + rh.try_hold(&buff_data[..5]).unwrap(); + assert!(rh.valid_request().is_none()); + } + + #[test] + fn try_appending_for_valid_request_uses_filled_data() { + let mut clean_buffer = [0u8; 256]; + let buff_data = get_buffer_with_valid_request(); + + let mut rh = RequestHolder::new(&mut clean_buffer); + rh.try_hold(&buff_data).unwrap(); + assert!(rh.valid_request().is_some()); + + rh.reset(); + assert!(rh.valid_request().is_none()); + + rh.try_hold(&buff_data[..5]).unwrap(); + assert!(rh.try_append_for_valid_request(&buff_data[5..10]).is_err()); + } + + #[test] + fn try_appending_for_valid_request_works() { + let mut clean_buffer = [0u8; 256]; + let buff_data = get_buffer_with_valid_request(); + println!("{buff_data:?}"); + + let mut rh = RequestHolder::new(&mut clean_buffer); + rh.try_hold(&buff_data).unwrap(); + assert!(rh.valid_request().is_some()); + + rh.reset(); + assert!(rh.valid_request().is_none()); + + rh.try_hold(&buff_data[..5]).unwrap(); + println!("before appending{rh:?}"); + let appending = rh.try_appending_for_valid_request(&buff_data[5..]); + // println!("{appending:?}",); + println!("after appending {rh:?}"); + assert!(appending.is_ok()); + } +} diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index ad52988b..0721d198 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -1,14 +1,12 @@ -use super::PartialWriteRequestTracker; - use crate::error::SftpError; use crate::handles::OpaqueFileHandle; use crate::proto::{ - self, InitVersionLowest, LStat, ReqId, SFTP_VERSION, SftpNum, SftpPacket, Stat, - StatusCode, + self, InitVersionClient, InitVersionLowest, LStat, ReqId, SFTP_VERSION, SftpNum, + SftpPacket, Stat, StatusCode, }; -use crate::requestholder::{RequestHolder, RequestHolderError}; use crate::server::{DirReply, ReadReply}; use crate::sftperror::SftpResult; +use crate::sftphandler::requestholder::{RequestHolder, RequestHolderError}; use crate::sftphandler::sftpoutputchannelhandler::{ SftpOutputPipe, SftpOutputProducer, }; @@ -27,24 +25,35 @@ use log::{debug, error, info, log, trace, warn}; /// FSM for handling sftp requests during [`SftpHandler::process`] #[derive(Default, Debug, PartialEq, Eq)] -enum SftpHandleState { - /// The handle is not initialized +enum HandlerState { + /// The handle is not been initialized. + /// if the client receivs an Init packet it will process it. #[default] - Initializing, - /// The handle is ready to process requests + Uninitialized, + /// The handle is ready to process requests. No request pending + /// A new packet will be evaluated to be process as: + /// - a regular request + /// - fragment (More data is needed) + /// - long request (It does not fit in the buffers and segmenting + /// strategies are used) Idle, - // /// The request is fragmented and needs special handling - // Fragmented(FragmentedRequestState), - /// A request that is clipped needs to be process. It cannot be decoded - /// and more bytes are needed - ProcessingClippedRequest, + /// The client has received a request and will decide how to process it. + /// Use the self.incomplete_request_holder + ProcessRequest { sftp_num: SftpNum }, + /// There is a fragmented request and more bytes are needed + /// Use the self.incomplete_request_holder + ProcessFragment, /// A request, with a length over the incoming buffer capacity is being /// processed. /// /// E.g. a write request with size exceeding the /// buffer size: Processing this request will require to be split /// into multiple write actions - ProcessingLongRequest, + ProcessWriteRequest { offset: u64, remaining_data: u32 }, + + /// Used to clear an invalid buffer in cases where there is still + /// data to be process but no longer required + ClearBuffer { data: usize }, } /// Process the raw buffers in and out from a subsystem channel decoding @@ -64,18 +73,20 @@ where S: SftpServer<'a, T>, { /// Holds the internal state if the SFTP handle - state: SftpHandleState, + state: HandlerState, /// The local SFTP File server implementing the basic SFTP requests /// defined by [`crate::sftpserver::SftpServer`] file_server: &'a mut S, - /// Use to process SFTP Write packets that have been received - /// partially and the remaining is expected in successive buffers - partial_write_request_tracker: Option>, - + // /// Use to process SFTP Write packets that have been received + // /// partially and the remaining is expected in successive buffers + // partial_write_request_tracker: Option>, /// Used to handle received buffers that do not hold a complete request [`SftpPacket`] - incomplete_request_holder: RequestHolder<'a>, + request_holder: RequestHolder<'a>, + + /// Marker to keep track of the OpaqueFileHandle type + _marker: core::marker::PhantomData, } impl<'a, T, S, const BUFFER_OUT_SIZE: usize> SftpHandler<'a, T, S, BUFFER_OUT_SIZE> @@ -97,364 +108,531 @@ where ) -> Self { SftpHandler { file_server, - partial_write_request_tracker: None, - state: SftpHandleState::default(), - incomplete_request_holder: RequestHolder::new(incomplete_request_buffer), + // partial_write_request_tracker: None, + state: HandlerState::default(), + request_holder: RequestHolder::new(incomplete_request_buffer), + _marker: core::marker::PhantomData, } } - /// WIP: A version of process that takes a chan_out to write the data - /// - /// Maintaining an internal status: - /// /// - Decodes the buffer_in request /// - Process the request delegating - /// operations to a [`SftpHandler::process`] implementation - /// - Serializes an answer in buffer_out + /// operations to a [`SftpServer`] implementation + /// - Serializes an answer in `output_producer` /// - /// **Returns**: A result containing the number of bytes used in - /// `buffer_out` async fn process( &mut self, buffer_in: &[u8], output_producer: &SftpOutputProducer<'_, BUFFER_OUT_SIZE>, ) -> SftpResult<()> { + /* + Possible scenarios: + - Init: The init handshake has to be performed. Only Init packet is accepted. NAV(Idle) + - handshake?: The client has received an Init packet and is processing it. NAV( Init, Idle) + - Idle: Ready to process request. No request pending. In this point. NAV(ProcessRequest, Fragment) + - Fragment: There is a fragmented request and more data is needed. NAV(ProcessRequest, ProcessLongRequest) + - ProcessRequest: The client has received a request and is processing it. NAV(Idle) + - ProcessLongRequest: The client has received a request that cannot fit in the buffer. Special treatment is required. NAV(Idle) + */ let mut buf = buffer_in; trace!("Received {:} bytes to process", buf.len()); + // We used `run_another_loop` to bypass the buf len check in + // cases where we need to process data held + // TODO: Fix this pattern + let mut skip_checking_buffer = false; trace!("Entering loop to process the full received buffer"); - while buf.len() > 0 { + while skip_checking_buffer || buf.len() > 0 { debug!( "<=======================[ SFTP Process State: {:?} ]=======================> Buffer remaining: {}", self.state, buf.len() ); - + skip_checking_buffer = false; match &self.state { - SftpHandleState::ProcessingClippedRequest => { - if let Err(e) = self - .incomplete_request_holder - .try_append_for_valid_request(&buf) - { - match e { - RequestHolderError::RanOut => { - debug!( - "There was not enough bytes in the buffer_in. \ - We will continue adding bytes" - ); - buf = &buf - [self.incomplete_request_holder.appended()..]; - continue; - } - RequestHolderError::WireError(WireError::RanOut) => { - debug!( - "WIRE ERROR: There was not enough bytes in the buffer_in. \ - We will continue adding bytes" - ); - buf = &buf - [self.incomplete_request_holder.appended()..]; - continue; - } - RequestHolderError::NoRoom => { - debug!( - "The request holder is full but the request in is incomplete. \ - We will try to decode it" - ); - } - - _ => { - error!( - "Unhandled error completing incomplete request {:?}", - e, - ); - return Err(SunsetError::Bug.into()); - } + HandlerState::ProcessWriteRequest { + offset, + remaining_data: data_len, + } => { + if let Some(request) = self.request_holder.valid_request() { + if let SftpPacket::Write(req_id, write) = request { + let used = (*data_len as usize).min(buf.len()); + let remaining_data = *data_len - used as u32; + + let data = &buf[..used]; + buf = &buf[used..]; + match self.file_server.write( + &T::try_from(&write.handle)?, + *offset, + data, + ) { + Ok(_) => { + if remaining_data == 0 { + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_OK, + "", + ) + .await?; + debug!("Still in buffer: {buf:?}"); + self.state = HandlerState::Idle; + } else { + self.state = + HandlerState::ProcessWriteRequest { + offset: *offset + (used as u64), + remaining_data, + }; + } + } + Err(e) => { + error!("SFTP write thrown: {:?}", e); + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_FAILURE, + "error writing", + ) + .await?; + self.state = HandlerState::ClearBuffer { + data: remaining_data as usize, + }; + } + }; + } else { + todo!("Wrong transition? Uncontrolled for now"); } } else { - debug!("Incomplete request holder completed the request!"); + todo!("Wrong transition? Uncontrolled for now"); } - - let used = self.incomplete_request_holder.appended(); - buf = &buf[used..]; - - let mut source = SftpSource::new( - &self.incomplete_request_holder.try_get_ref()?, - ); - trace!("Internal Source Content: {:?}", source); + } + HandlerState::Uninitialized => { + debug!("Creating a source: buf_len = {:?}", buf.len()); + let mut source = SftpSource::new(&buf); match SftpPacket::decode_request(&mut source) { - Ok(request) => { - Self::handle_general_request( - &mut self.file_server, - output_producer, - request, - ) - .await?; - self.incomplete_request_holder.reset(); - self.state = SftpHandleState::Idle; - } - Err(e) => match e { - WireError::UnknownPacket { number } => { - warn!( - "Unknown packet: packetId = {:?}. Will flush \ - its length and send unsupported back", - number + Ok(request) => match request { + SftpPacket::Init(InitVersionClient { + version: SFTP_VERSION, + }) => { + debug!( + "Accepted initialization request: {:?}", + request ); - - let req_id = ReqId(source.peak_packet_req_id()?); - let len = source.peak_total_packet_len()? as usize; - source.consume_first(len)?; output_producer - .send_status( - req_id, - StatusCode::SSH_FX_OP_UNSUPPORTED, - "Error decoding SFTP Packet", - ) + .send_packet(&SftpPacket::Version( + InitVersionLowest { version: SFTP_VERSION }, + )) .await?; + buf = &buf[buf.len() - source.remaining()..]; + self.state = HandlerState::Idle; } - WireError::RanOut => match Self::handle_ran_out( - &mut self.file_server, - output_producer, - &mut source, - ) - .await - { - Ok(holder) => { - self.partial_write_request_tracker = - Some(holder); - self.incomplete_request_holder.reset(); - self.state = - SftpHandleState::ProcessingLongRequest; - } - Err(e) => match e { - _ => { - error!( - "handle_ran_out finished with error: {:?}", - e - ); - return Err(SunsetError::Bug.into()); - } - }, - }, - WireError::NoRoom => { - error!("Not enough space to fit the request") + SftpPacket::Init(init_version_client) => { + error!( + "Incompatible SFTP Version: {:?} is not {SFTP_VERSION:?}", + &init_version_client + ); + return Err(SftpError::NotSupported); } _ => { error!( - "Unhandled error decoding assembled packet: {:?}", - e + "Wrong SFTP Packet before Init or incompatible version: {request:?}" ); - return Err(WireError::PacketWrong.into()); + return Err(SftpError::NotInitialized); } }, + Err(e) => { + error!("Malformed SFTP Packet before Init: {e:?}"); + return Err(SftpError::MalformedPacket); + } // Err(e) => { + // error!("Malformed SFTP Packet before Init: {e:?}"); + // return Err(SftpError::MalformedPacket); + // } } } - SftpHandleState::ProcessingLongRequest => { + HandlerState::Idle => { + self.request_holder.reset(); + debug!("Creating a source: buf_len = {:?}", buf.len()); let mut source = SftpSource::new(&buf); - trace!("Source content: {:?}", source); - - let mut write_tracker = if let Some(wt) = - self.partial_write_request_tracker.take() - { - wt - } else { - error!( - "BUG: SftpHandleState::ProcessingLongRequest cannot take the write tracker" - ); - return Err(SunsetError::Bug.into()); - }; - - let opaque_handle = write_tracker.get_opaque_file_handle(); + debug!("source: {source:?}"); - let usable_data = source - .remaining() - .min(write_tracker.get_remain_data_len() as usize); - - let data_segment = source.dec_as_binstring(usable_data)?; - - let data_segment_len = u32::try_from(data_segment.0.len()) - .map_err(|e| { - error!("Error casting data segment len to u32: {e}"); - SunsetError::Bug - })?; - let current_write_offset = - write_tracker.get_remain_data_offset(); - write_tracker - .update_remaining_after_partial_write(data_segment_len); + match SftpPacket::decode_request(&mut source) { + Ok(request) => { + debug!("Got a valid request {:?}", request.sftp_num()); + self.request_holder.try_hold(&source.buffer_used())?; - debug!( - "Processing successive chunks of a long write packet. \ - Writing : opaque_handle = {:?}, write_offset = {:?}, \ - data_segment = {:?}, data remaining = {:?}", - opaque_handle, - current_write_offset, - data_segment, - write_tracker.get_remain_data_len() - ); + buf = &buf[buf.len() - source.remaining()..]; - match self.file_server.write( - &opaque_handle, - current_write_offset, - data_segment.as_ref(), - ) { - Ok(_) => { - if write_tracker.get_remain_data_len() > 0 { - self.partial_write_request_tracker = - Some(write_tracker); - } else { - output_producer - .send_status( - write_tracker.get_req_id(), - StatusCode::SSH_FX_OK, - "", - ) - .await?; - debug!("Finished multi part Write Request"); - self.state = SftpHandleState::Idle; - } + // We got the request. Moving on to process it before deserializing more + // data + skip_checking_buffer = true; + self.state = HandlerState::ProcessRequest { + sftp_num: request.sftp_num(), + }; + // TODO Wasteful. Will have to decode the request again. Maybe hold it? + buf = &buf[buf.len() - source.remaining()..]; } - Err(e) => { - error!("SFTP write thrown: {:?}", e); + Err(WireError::RanOut) => { + debug!("source: {source:?}"); + let rl = self + .request_holder + .try_hold(&source.consume_all())?; + + buf = &buf[buf.len() - source.remaining()..]; + debug!( + "Incomplete packet. request holder initialized with {rl:?} bytes" + ); + self.state = HandlerState::ProcessFragment; + } + Err(WireError::UnknownPacket { number }) => { + error!("Unknown packet: {number}"); + output_producer + .send_status( + ReqId( + source + .peak_packet_req_id() + .unwrap_or(u32::MAX), + ), + StatusCode::SSH_FX_OP_UNSUPPORTED, + "", + ) + .await?; + } + Err(WireError::PacketWrong) => { + error!("Not a request: "); output_producer .send_status( - write_tracker.get_req_id(), - StatusCode::SSH_FX_FAILURE, - "error writing", + ReqId( + source + .peak_packet_req_id() + .unwrap_or(u32::MAX), + ), + StatusCode::SSH_FX_BAD_MESSAGE, + "Not a request", ) .await?; - self.state = SftpHandleState::Idle; + } + Err(e) => { + error!("Unexpected error: Bug!"); + return Err(SftpError::WireError(e)); } }; - buf = &buf[buf.len() - source.remaining()..]; } - SftpHandleState::Initializing => { - let (source, sftp_packet) = create_sftp_source_and_packet(buf); - match sftp_packet { - Ok(request) => { - match request { - SftpPacket::Init(_) => { - let version = - SftpPacket::Version(InitVersionLowest { - version: SFTP_VERSION, - }); - - output_producer.send_packet(&version).await?; - self.state = SftpHandleState::Idle; - } - _ => { - error!( - "Request received before init: {:?}", - request - ); - return Err(SftpError::NotInitialized); - } - }; + HandlerState::ProcessFragment => { + match self.request_holder.try_appending_for_valid_request(&buf) { + Ok(sftp_num) => { + let used = self.request_holder.appended(); + debug!( + "{used:?} bytes added. We got a complete request: {sftp_num:?}:: {:?}", + self.request_holder + ); + debug!( + "Request: {:?}", + self.request_holder.valid_request() + ); + buf = &buf[used..]; + self.state = HandlerState::ProcessRequest { sftp_num } } - Err(_) => { + Err(RequestHolderError::RanOut) => { + let used = self.request_holder.appended(); + buf = &buf[used..]; + debug!( + "{used:?} bytes added. Will keep adding \ + until we hold a valid request" + ); + } + Err(RequestHolderError::NoRoom) => { error!( - "Malformed SFTP Packet before Init: {:?}", - sftp_packet + "Could not complete the request. holding buffer is full" ); - return Err(SftpError::MalformedPacket); + return Err(SunsetError::Bug.into()); + } + Err(e) => { + error!("{e:?}"); + return Err(e.into()); } } - buf = &buf[buf.len() - source.remaining()..]; } - SftpHandleState::Idle => { - let (mut source, sftp_packet) = - create_sftp_source_and_packet(buf); - match sftp_packet { - Ok(request) => { - Self::handle_general_request( - &mut self.file_server, - output_producer, - request, - ) - .await?; - } - Err(e) => match e { - WireError::RanOut => { - debug!( - "The packet cannot fit in the receiving buffer: It will be handled in parts ({:?})", - e - ); + HandlerState::ProcessRequest { .. } => { + // At this point the assumption is that the request holder will contain + // a full valid request (Lets call this an invariant) - match Self::handle_ran_out( - &mut self.file_server, - output_producer, - &mut source, - ) - .await + if let Some(request) = self.request_holder.valid_request() { + if !request.sftp_num().is_request() { + error!( + "Unexpected SftpPacket: {:?}", + request.sftp_num() + ); + return Err(SunsetError::BadUsage {}.into()); + } + match request { + // SftpPacket::Init(init_version_client) => todo!(), + // SftpPacket::Version(init_version_lowest) => todo!(), + SftpPacket::Read(req_id, read) => { + if let Err(error) = self + .file_server + .read( + &T::try_from(&read.handle)?, + read.offset, + read.len, + &ReadReply::new(req_id, output_producer), + ) + .await { - Ok(holder) => { - self.partial_write_request_tracker = - Some(holder); - self.state = - SftpHandleState::ProcessingLongRequest; + error!("Error reading data: {:?}", error); + if let SftpError::FileServerError(status) = error + { + output_producer + .send_status( + req_id, + status, + "Could not list attributes", + ) + .await?; + } else { + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_FAILURE, + "Could not list attributes", + ) + .await?; } - Err(e) => { - debug!("Error handle_ran_out : {:?}", e); - match e { - SftpError::WireError( - WireError::RanOut, - ) => { - let read = self - .incomplete_request_holder - .try_hold(&buf)?; - buf = &buf[read..]; + }; + self.state = HandlerState::Idle; + } + SftpPacket::LStat(req_id, LStat { file_path: path }) => { + match self.file_server.stats(false, path.as_str()?) { + Ok(attrs) => { + debug!( + "List stats for {} is {:?}", + path, attrs + ); - self.state = SftpHandleState::ProcessingClippedRequest; - continue; - } - _ => { - error!( - "Error handle_ran_out: No Could not be catch {:?}", - e - ); - return Err(SunsetError::Bug.into()); - } - } + output_producer + .send_packet(&SftpPacket::Attrs( + req_id, attrs, + )) + .await?; + } + Err(status) => { + error!( + "Error listing stats for {}: {:?}", + path, status + ); + output_producer + .send_status( + req_id, + status, + "Could not list attributes", + ) + .await?; } }; + self.state = HandlerState::Idle; } - WireError::UnknownPacket { number } => { - warn!( - "Unknown packet: packetId = {:?}. Will flush \ - its length and send unsupported back", - number - ); + SftpPacket::Stat(req_id, Stat { file_path: path }) => { + match self.file_server.stats(true, path.as_str()?) { + Ok(attrs) => { + debug!( + "List stats for {} is {:?}", + path, attrs + ); - let req_id = ReqId(source.peak_packet_req_id()?); - let len = source.peak_total_packet_len()? as usize; - source.consume_first(len)?; - output_producer - .send_status( - req_id, - StatusCode::SSH_FX_OP_UNSUPPORTED, - "Error decoding SFTP Packet", + output_producer + .send_packet(&SftpPacket::Attrs( + req_id, attrs, + )) + .await?; + } + Err(status) => { + error!( + "Error listing stats for {}: {:?}", + path, status + ); + output_producer + .send_status( + req_id, + status, + "Could not list attributes", + ) + .await?; + } + }; + self.state = HandlerState::Idle; + } + SftpPacket::ReadDir(req_id, read_dir) => { + if let Err(status) = self + .file_server + .readdir( + &T::try_from(&read_dir.handle)?, + &DirReply::new(req_id, output_producer), ) - .await?; + .await + { + error!("Open failed: {:?}", status); + + output_producer + .send_status( + req_id, + status, + "Error Reading Directory", + ) + .await?; + }; + self.state = HandlerState::Idle; + } + SftpPacket::OpenDir(req_id, open_dir) => { + match self + .file_server + .opendir(open_dir.dirname.as_str()?) + { + Ok(opaque_file_handle) => { + let response = SftpPacket::Handle( + req_id, + proto::Handle { + handle: opaque_file_handle + .into_file_handle(), + }, + ); + output_producer + .send_packet(&response) + .await?; + } + Err(status_code) => { + error!("Open failed: {:?}", status_code); + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_FAILURE, + "", + ) + .await?; + } + }; + self.state = HandlerState::Idle; + } + SftpPacket::Close(req_id, close) => { + match self + .file_server + .close(&T::try_from(&close.handle)?) + { + Ok(_) => { + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_OK, + "", + ) + .await?; + } + Err(e) => { + error!("SFTP Close thrown: {:?}", e); + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_FAILURE, + "Could not Close the handle", + ) + .await?; + } + } + self.state = HandlerState::Idle; + } + SftpPacket::Write(_, write) => { + debug!("Got write: {:?}", write); + self.state = HandlerState::ProcessWriteRequest { + offset: write.offset, + remaining_data: write.data_len, + }; + } + SftpPacket::Open(req_id, open) => { + match self + .file_server + .open(open.filename.as_str()?, &open.pflags) + { + Ok(opaque_file_handle) => { + let response = SftpPacket::Handle( + req_id, + proto::Handle { + handle: opaque_file_handle + .into_file_handle(), + }, + ); + output_producer + .send_packet(&response) + .await?; + } + Err(status_code) => { + error!("Open failed: {:?}", status_code); + output_producer + .send_status( + req_id, + StatusCode::SSH_FX_FAILURE, + "", + ) + .await?; + } + }; + self.state = HandlerState::Idle; + } + SftpPacket::PathInfo(req_id, path_info) => { + match self + .file_server + .realpath(path_info.path.as_str()?) + { + Ok(name_entry) => { + let dir_reply = + DirReply::new(req_id, output_producer); + let encoded_len = + crate::sftpserver::helpers::get_name_entry_len(&name_entry)?; + debug!( + "PathInfo encoded length: {:?}", + encoded_len + ); + trace!( + "PathInfo Response content: {:?}", + encoded_len + ); + dir_reply + .send_header(1, encoded_len) + .await?; + dir_reply.send_item(&name_entry).await?; + } + Err(code) => { + output_producer + .send_status(req_id, code, "") + .await?; + } + } + self.state = HandlerState::Idle; } + // SftpPacket::Status(req_id, status) => todo!(), + // SftpPacket::Handle(req_id, handle) => todo!(), + // SftpPacket::Data(req_id, data) => todo!(), + // SftpPacket::Name(req_id, name) => todo!(), + // SftpPacket::Attrs(req_id, attrs) => todo!(), _ => { - error!("Error decoding SFTP Packet: {:?}", e); - output_producer - .send_status( - ReqId(u32::MAX), - StatusCode::SSH_FX_OP_UNSUPPORTED, - "Error decoding SFTP Packet", - ) - .await?; + + // TODO: Use a catch all } - }, - }; - buf = &buf[buf.len() - source.remaining()..]; - trace!("New buffer len {} bytes ", buf.len()) + } + } else { + return Err(SunsetError::bug().into()); + } + } + HandlerState::ClearBuffer { data } => { + if *data == 0 { + self.state = HandlerState::Idle; + } else { + buf = &buf[(*data).min(buf.len())..] + } } } - trace!("Process checking buf len {:?}", buf.len()); + trace!("Process will check buf len {:?}", buf.len()); } - trace!("Exiting process with Ok(())"); + debug!("Whole buffer processed. Getting more data"); Ok(()) } @@ -511,296 +689,4 @@ where } } } - - /// Handles Healthy formed SftpRequest. Will return error if: - /// - /// - The request (SftpPacket) is not a request - /// - /// - The request is an unknown SftpPacket - /// - /// - The request is an initialization packet, and the initialization - /// has already been performed - async fn handle_general_request( - file_server: &mut S, - output_producer: &SftpOutputProducer<'_, BUFFER_OUT_SIZE>, - request: SftpPacket<'_>, - ) -> Result<(), SftpError> - where - T: OpaqueFileHandle, - { - debug!("Handling general request: {:?}", request); - match request { - SftpPacket::Init(_) => { - error!("The Init packet is not a request but an initialization"); - return Err(SftpError::AlreadyInitialized); - } - SftpPacket::PathInfo(req_id, path_info) => { - let dir_reply = DirReply::new(req_id, output_producer); - let name_entry = file_server.realpath(path_info.path.as_str()?)?; - - let encoded_len = - crate::sftpserver::helpers::get_name_entry_len(&name_entry)?; - debug!("PathInfo encoded length: {:?}", encoded_len); - trace!("PathInfo Response content: {:?}", encoded_len); - dir_reply.send_header(1, encoded_len).await?; - dir_reply.send_item(&name_entry).await?; - } - SftpPacket::Open(req_id, open) => { - match file_server.open(open.filename.as_str()?, &open.pflags) { - Ok(opaque_file_handle) => { - let response = SftpPacket::Handle( - req_id, - proto::Handle { - handle: opaque_file_handle.into_file_handle(), - }, - ); - output_producer.send_packet(&response).await?; - } - Err(status_code) => { - error!("Open failed: {:?}", status_code); - output_producer - .send_status(req_id, StatusCode::SSH_FX_FAILURE, "") - .await?; - } - }; - } - // TODO The visitor behavioral pattern could be use in write to speed-up - // the writing process - SftpPacket::Write(req_id, write) => { - match file_server.write( - &T::try_from(&write.handle)?, - write.offset, - write.data.as_ref(), - ) { - Ok(_) => { - output_producer - .send_status(req_id, StatusCode::SSH_FX_OK, "") - .await?; - } - Err(e) => { - error!("SFTP write thrown: {:?}", e); - output_producer - .send_status( - req_id, - StatusCode::SSH_FX_FAILURE, - "error writing", - ) - .await?; - } - }; - } - SftpPacket::Close(req_id, close) => { - match file_server.close(&T::try_from(&close.handle)?) { - Ok(_) => { - output_producer - .send_status(req_id, StatusCode::SSH_FX_OK, "") - .await?; - } - Err(e) => { - error!("SFTP Close thrown: {:?}", e); - output_producer - .send_status( - req_id, - StatusCode::SSH_FX_FAILURE, - "Could not Close the handle", - ) - .await?; - } - } - } - SftpPacket::OpenDir(req_id, open_dir) => { - match file_server.opendir(open_dir.dirname.as_str()?) { - Ok(opaque_file_handle) => { - let response = SftpPacket::Handle( - req_id, - proto::Handle { - handle: opaque_file_handle.into_file_handle(), - }, - ); - output_producer.send_packet(&response).await?; - } - Err(status_code) => { - error!("Open failed: {:?}", status_code); - output_producer - .send_status(req_id, StatusCode::SSH_FX_FAILURE, "") - .await?; - } - }; - } - SftpPacket::ReadDir(req_id, read_dir) => { - if let Err(status) = file_server - .readdir( - &T::try_from(&read_dir.handle)?, - &DirReply::new(req_id, output_producer), - ) - .await - { - error!("Open failed: {:?}", status); - - output_producer - .send_status(req_id, status, "Error Reading Directory") - .await?; - }; - } - SftpPacket::Read(req_id, read) => { - if let Err(error) = file_server - .read( - &T::try_from(&read.handle)?, - read.offset, - read.len, - &ReadReply::new(req_id, output_producer), - ) - .await - { - error!("Error reading data: {:?}", error); - if let SftpError::FileServerError(status) = error { - output_producer - .send_status(req_id, status, "Could not list attributes") - .await?; - } else { - output_producer - .send_status( - req_id, - StatusCode::SSH_FX_FAILURE, - "Could not list attributes", - ) - .await?; - } - } - } - SftpPacket::LStat(req_id, LStat { file_path: path }) => { - match file_server.stats(false, path.as_str()?) { - Ok(attrs) => { - debug!("List stats for {} is {:?}", path, attrs); - - output_producer - .send_packet(&SftpPacket::Attrs(req_id, attrs)) - .await?; - } - Err(status) => { - error!("Error listing stats for {}: {:?}", path, status); - output_producer - .send_status(req_id, status, "Could not list attributes") - .await?; - } - } - } - SftpPacket::Stat(req_id, Stat { file_path: path }) => { - match file_server.stats(true, path.as_str()?) { - Ok(attrs) => { - debug!("List stats for {} is {:?}", path, attrs); - - output_producer - .send_packet(&SftpPacket::Attrs(req_id, attrs)) - .await?; - } - Err(status) => { - error!("Error listing stats for {}: {:?}", path, status); - output_producer - .send_status(req_id, status, "Could not list attributes") - .await?; - } - } - } - _ => { - error!("Unsupported request type: {:?}", request); - return Err(SftpError::NotSupported); - } - } - Ok(()) - } - - // TODO Handle other long requests - /// Some long request will not fit in the channel buffers. Such requests - /// will require to be handled differently. Gathering the data in and - /// processing it as we receive it in the channel in buffer. - /// - /// In the current approach, a tracker is required to store the state of - /// the processing of such long requests. - /// - /// With an implementation that where able to hold the channel_in there might - /// be no need to keep this tracker. - /// - /// **WARNING:** Only `SSH_FXP_WRITE` has been implemented! - /// - async fn handle_ran_out( - file_server: &mut S, - output_producer: &SftpOutputProducer<'_, BUFFER_OUT_SIZE>, - source: &mut SftpSource<'_>, - ) -> SftpResult> { - debug!("Handing Ran out"); - let packet_type = source.peak_packet_type()?; - match packet_type { - SftpNum::SSH_FXP_WRITE => { - debug!( - "about to decode packet partial write content. Source remaining = {:?}", - source.remaining() - ); - let ( - obscured_file_handle, - req_id, - offset, - data_in_buffer, - write_tracker, - ) = source - .dec_packet_partial_write_content_and_get_tracker::()?; - - trace!( - "obscured_file_handle = {:?}, req_id = {:?}, \ - offset = {:?}, data_in_buffer = {:?}, \ - write_tracker = {:?}", - obscured_file_handle, - req_id, - offset, - data_in_buffer, - write_tracker, - ); - - match file_server.write( - &obscured_file_handle, - offset, - data_in_buffer.as_ref(), - ) { - Ok(_) => { - debug!( - "Storing a write tracker for a fragmented write request" - ); - return Ok(write_tracker); - } - Err(e) => { - error!("SFTP write thrown: {:?}", e); - output_producer - .send_status( - req_id, - StatusCode::SSH_FX_FAILURE, - "error writing ", - ) - .await?; - return Err(SftpError::FileServerError(e)); - } - }; - } - _ => { - error!( - "RanOut of Packet type could not be handled {:?}", - packet_type - ); - return Err(SftpError::NotSupported); - } - }; - // Ok(()) - } -} - -/// Function to create an SFTP source and decode an SFTP packet from it. -/// -/// Defined to avoid code duplication. -fn create_sftp_source_and_packet( - buf: &[u8], -) -> (SftpSource<'_>, Result, WireError>) { - debug!("Creating a source: buf_len = {:?}", buf.len()); - let mut source = SftpSource::new(&buf); - - let sftp_packet = SftpPacket::decode_request(&mut source); - (source, sftp_packet) } diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index af17b38a..215bab5e 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,7 +1,7 @@ use crate::error::{SftpError, SftpResult}; use crate::proto::{ ENCODED_BASE_DATA_SFTP_PACKET_LENGTH, ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, - MAX_NAME_ENTRY_SIZE, NameEntry, PFlags, SftpNum, SftpPacket, + MAX_NAME_ENTRY_SIZE, NameEntry, PFlags, SftpNum, }; use crate::server::SftpSink; use crate::sftphandler::SftpOutputProducer; diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 8e70c589..35b18ac6 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -1,14 +1,9 @@ -use crate::error::{SftpError, SftpResult}; -use crate::handles::OpaqueFileHandle; use crate::proto::{ - ReqId, SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, - SFTP_FIELD_REQ_ID_INDEX, SFTP_FIELD_REQ_ID_LEN, SFTP_MINIMUM_PACKET_LEN, - SFTP_WRITE_REQID_INDEX, SftpNum, + SFTP_FIELD_ID_INDEX, SFTP_FIELD_LEN_INDEX, SFTP_FIELD_LEN_LENGTH, + SFTP_FIELD_REQ_ID_INDEX, SFTP_FIELD_REQ_ID_LEN, SftpNum, }; -use crate::protocol::FileHandle; -use crate::sftphandler::PartialWriteRequestTracker; -use sunset::sshwire::{BinString, SSHDecode, SSHSource, WireError, WireResult}; +use sunset::sshwire::{SSHSource, WireError, WireResult}; #[allow(unused_imports)] use log::{debug, error, info, log, trace, warn}; @@ -101,9 +96,6 @@ impl<'de> SftpSource<'de> { /// /// This does not advance the reading index /// - /// This does not consider the length field itself - /// Useful to observe the packet fields in special conditions where a `dec(s)` - /// would fail /// /// **Warning**: will only work in well formed packets, in other case the result /// will contains garbage @@ -117,76 +109,11 @@ impl<'de> SftpSource<'de> { /// in the source /// **Warning**: will only work in well formed packets, in other case /// the result will contains garbage - pub fn packet_fits(&self) -> WireResult { - Ok(self.buffer.len() >= self.peak_total_packet_len()? as usize) - } - - /// Assuming that the buffer contains a [`proto::Write`] request packet initial - /// bytes and not its totality: - /// - /// **Returns**: - /// - /// - An [`OpaqueFileHandle`] to guide the Write operation, - /// - Request ID as [`ReqId`], - /// - Offset as [`u64`] - /// - Data in the buffer as [`BinString`] - /// - [`PartialWriteRequestTracker`] to handle subsequent portions of the request - /// - /// **Warning**: will only work in well formed write packets, in other case - /// the result will contains garbage - pub(crate) fn dec_packet_partial_write_content_and_get_tracker< - T: OpaqueFileHandle, - >( - &mut self, - ) -> SftpResult<(T, ReqId, u64, BinString<'de>, PartialWriteRequestTracker)> - { - if self.buffer.len() < SFTP_MINIMUM_PACKET_LEN { - return Err(WireError::RanOut.into()); + pub fn packet_fits(&self) -> bool { + match self.peak_total_packet_len() { + Ok(len) => self.buffer.len() >= len as usize, + Err(_) => false, } - - match self.peak_packet_type()? { - SftpNum::SSH_FXP_WRITE => {} - _ => return Err(SftpError::NotSupported), - }; - - self.index = SFTP_WRITE_REQID_INDEX; - let req_id = ReqId::dec(self)?; - let file_handle = FileHandle::dec(self)?; - - let offset = u64::dec(self)?; - let data_len = u32::dec(self)?; - - let data_len_in_buffer = self.buffer.len() - self.index; - let data_in_buffer = BinString(self.take(data_len_in_buffer)?); - - let remain_data_len = data_len - data_len_in_buffer as u32; - let remain_data_offset = offset + data_len_in_buffer as u64; - trace!( - "Request ID = {:?}, Handle = {:?}, offset = {:?}, data length in buffer = {:?}, data in current buffer {:?} ", - req_id, file_handle, offset, data_len_in_buffer, data_in_buffer - ); - - let write_tracker = PartialWriteRequestTracker::new( - req_id, - OpaqueFileHandle::try_from(&file_handle)?, - remain_data_len, - remain_data_offset, - )?; - - let obscured_file_handle = OpaqueFileHandle::try_from(&file_handle)?; - Ok((obscured_file_handle, req_id, offset, data_in_buffer, write_tracker)) - } - - /// Used to decode a slice of [`SSHSource`] as a single BinString - /// - /// It will not use the first four bytes as u32 for length, instead - /// it will use the length of the data received and use it to set the - /// length of the returned BinString. - pub(crate) fn dec_as_binstring( - &mut self, - len: usize, - ) -> WireResult> { - Ok(BinString(self.take(len)?)) } pub fn peak_packet_req_id(&self) -> WireResult { @@ -202,14 +129,15 @@ impl<'de> SftpSource<'de> { } } - /// Discards the first elements of the - pub fn consume_first(&mut self, len: usize) -> WireResult<()> { - if len > self.buffer.len() { - Err(WireError::RanOut) - } else { - self.index = len; - Ok(()) - } + pub fn buffer_used(&self) -> &[u8] { + &self.buffer[..self.index] + } + + /// returns a slice on the held buffer and makes it unavailable for further + /// decodes. + pub fn consume_all(&mut self) -> &[u8] { + self.index = self.buffer.len(); + self.buffer } } @@ -234,18 +162,18 @@ mod local_tests { #[test] fn peaking_len() { let buffer_status = status_buffer(); - let sink = SftpSource::new(&buffer_status); + let source = SftpSource::new(&buffer_status); - let read_packet_len = sink.peak_packet_len().unwrap(); + let read_packet_len = source.peak_packet_len().unwrap(); let original_packet_len = 23u32; assert_eq!(original_packet_len, read_packet_len); } #[test] fn peaking_total_len() { let buffer_status = status_buffer(); - let sink = SftpSource::new(&buffer_status); + let source = SftpSource::new(&buffer_status); - let read_total_packet_len = sink.peak_total_packet_len().unwrap(); + let read_total_packet_len = source.peak_total_packet_len().unwrap(); let original_total_packet_len = 23u32 + 4u32; assert_eq!(original_total_packet_len, read_total_packet_len); } @@ -253,16 +181,16 @@ mod local_tests { #[test] fn peaking_type() { let buffer_status = status_buffer(); - let sink = SftpSource::new(&buffer_status); - let read_packet_type = sink.peak_packet_type().unwrap(); + let source = SftpSource::new(&buffer_status); + let read_packet_type = source.peak_packet_type().unwrap(); let original_packet_type = SftpNum::from(101u8); assert_eq!(original_packet_type, read_packet_type); } #[test] fn peaking_req_id() { let buffer_status = status_buffer(); - let sink = SftpSource::new(&buffer_status); - let read_req_id = sink.peak_packet_req_id().unwrap(); + let source = SftpSource::new(&buffer_status); + let read_req_id = source.peak_packet_req_id().unwrap(); let original_req_id = 16u32; assert_eq!(original_req_id, read_req_id); } @@ -270,15 +198,31 @@ mod local_tests { #[test] fn packet_does_fit() { let buffer_status = status_buffer(); - let sink = SftpSource::new(&buffer_status); - assert_eq!(true, sink.packet_fits().unwrap()); + let source = SftpSource::new(&buffer_status); + assert_eq!(true, source.packet_fits()); } #[test] fn packet_does_not_fit() { let buffer_status = status_buffer(); let no_room_buffer = &buffer_status[..buffer_status.len() - 2]; - let sink = SftpSource::new(no_room_buffer); - assert_eq!(false, sink.packet_fits().unwrap()); + let source = SftpSource::new(no_room_buffer); + assert_eq!(false, source.packet_fits()); + } + + #[test] + fn consume_all_remaining() { + let inc_array: [u8; 512] = core::array::from_fn(|i| (i % 255) as u8); + let mut source = SftpSource::new(&inc_array); + let _consumed = source.consume_all(); + assert_eq!(0usize, source.remaining()); + } + + #[test] + fn consume_all_consumed() { + let inc_array: [u8; 512] = core::array::from_fn(|i| (i % 255) as u8); + let mut source = SftpSource::new(&inc_array); + let consumed = source.consume_all(); + assert_eq!(inc_array.len(), consumed.len()); } } From 8ae36fcf29a7b8a28b02b7772a925471a72c3496 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 25 Nov 2025 16:44:43 +1100 Subject: [PATCH 371/393] [skip ci] Adding more unit tests. Noted that it would be nice improving encoding of SSH_FXP_DATA and SSH_FXP_NAME packages --- sftp/src/proto.rs | 2 +- sftp/src/sftphandler/requestholder.rs | 2 +- sftp/src/sftpserver.rs | 128 ++++++++++++++++++++++---- 3 files changed, 110 insertions(+), 22 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 6af64d76..2bb6045a 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -1059,7 +1059,7 @@ mod proto_tests { println!("source = {:?}", source); match SftpPacket::decode_request(&mut source) { - Ok(SftpPacket::Open(req_id, open)) => { + Ok(SftpPacket::Open(_req_id, open)) => { assert_eq!(PFlags::SSH_FXF_READ, open.pflags); } Ok(other) => panic!("Expected Open packet, got: {:?}", other), diff --git a/sftp/src/sftphandler/requestholder.rs b/sftp/src/sftphandler/requestholder.rs index 4fa65d4f..1a02a381 100644 --- a/sftp/src/sftphandler/requestholder.rs +++ b/sftp/src/sftphandler/requestholder.rs @@ -340,7 +340,7 @@ mod local_test { assert!(rh.valid_request().is_none()); rh.try_hold(&buff_data[..5]).unwrap(); - assert!(rh.try_append_for_valid_request(&buff_data[5..10]).is_err()); + assert!(rh.try_appending_for_valid_request(&buff_data[5..10]).is_err()); } #[test] diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 215bab5e..133794cc 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -189,7 +189,8 @@ impl<'g, const N: usize> ReadReply<'g, N> { } // TODO Make this enforceable - /// Sends a header fro `SSH_FXP_DATA` response. This includes the total + // TODO Automate encoding the SftpPacket + /// Sends a header for `SSH_FXP_DATA` response. This includes the total /// response length, the packet type, request id and data length /// /// The packet data content, excluding the length must be sent using @@ -202,16 +203,10 @@ impl<'g, const N: usize> ReadReply<'g, N> { let mut s = [0u8; N]; let mut sink = SftpSink::new(&mut s); - // Encoding length field - (data_len + ENCODED_BASE_DATA_SFTP_PACKET_LENGTH).enc(&mut sink)?; - // Encoding packet type - u8::from(SftpNum::SSH_FXP_DATA).enc(&mut sink)?; - // Encoding req_id - self.req_id.enc(&mut sink)?; - // string data length - data_len.enc(&mut sink)?; + let payload = + ReadReply::::encode_data_header(&mut sink, self.req_id, data_len)?; - let payload = sink.payload_slice(); + // let payload = sink.payload_slice(); debug!( "Sending header: len = {:?}, content = {:?}", payload.len(), @@ -246,6 +241,49 @@ impl<'g, const N: usize> ReadReply<'g, N> { .send_status(ReqId(self.req_id.0 + 1), StatusCode::SSH_FX_EOF, "") .await } + + fn encode_data_header( + sink: &'g mut SftpSink<'g>, + req_id: ReqId, + data_len: u32, + ) -> Result<&'g [u8], SftpError> { + (data_len + ENCODED_BASE_DATA_SFTP_PACKET_LENGTH).enc(sink)?; + u8::from(SftpNum::SSH_FXP_DATA).enc(sink)?; + req_id.enc(sink)?; + data_len.enc(sink)?; + Ok(sink.payload_slice()) + } +} + +#[cfg(test)] +mod read_reply_tests { + use super::*; + + #[cfg(test)] + extern crate std; + // #[cfg(test)] + // use std::println; + + #[test] + fn compose_header() { + const N: usize = 512; + + let req_id = ReqId(42); + let data_len = 128; + let mut buffer = [0u8; N]; + let mut sink = SftpSink::new(&mut buffer); + + let payload = + ReadReply::::encode_data_header(&mut sink, req_id, data_len).unwrap(); + + // println!("{payload:?}"); + + // println!("{:?}", &u32::from_be_bytes(payload[..4].try_into().unwrap())); + assert_eq!( + data_len + ENCODED_BASE_DATA_SFTP_PACKET_LENGTH, + u32::from_be_bytes(payload[..4].try_into().unwrap()) + ); + } } /// Uses for [`DirReply`] to: @@ -283,6 +321,7 @@ impl<'g, const N: usize> DirReply<'g, N> { } // TODO Make this enforceable + // TODO Automate encoding the SftpPacket /// Sends the header to the client with the number of files as [`NameEntry`] and the [`SSHEncode`] /// length of all these [`NameEntry`] items pub async fn send_header( @@ -297,20 +336,19 @@ impl<'g, const N: usize> DirReply<'g, N> { let mut s = [0u8; N]; let mut sink = SftpSink::new(&mut s); - // We need to consider the packet type, Id and count fields - // This way I collect data required for the header and collect - // valid entries into a vector (only std) - (items_encoded_len + ENCODED_BASE_NAME_SFTP_PACKET_LENGTH).enc(&mut sink)?; - u8::from(SftpNum::SSH_FXP_NAME).enc(&mut sink)?; - self.req_id.enc(&mut sink)?; - count.enc(&mut sink)?; - let payload = sink.payload_slice(); + let payload = DirReply::::encode_data_header( + &mut sink, + self.req_id, + items_encoded_len, + count, + )?; + debug!( "Sending header: len = {:?}, content = {:?}", payload.len(), payload ); - self.chan_out.send_data(sink.payload_slice()).await?; + self.chan_out.send_data(payload).await?; Ok(()) } @@ -332,6 +370,56 @@ impl<'g, const N: usize> DirReply<'g, N> { pub async fn send_eof(&self) -> SftpResult<()> { self.chan_out.send_status(self.req_id, StatusCode::SSH_FX_EOF, "").await } + + fn encode_data_header( + sink: &'g mut SftpSink<'g>, + req_id: ReqId, + items_encoded_len: u32, + count: u32, + ) -> Result<&'g [u8], SftpError> { + // We need to consider the packet type, Id and count fields + // This way I collect data required for the header and collect + // valid entries into a vector (only std) + (items_encoded_len + ENCODED_BASE_NAME_SFTP_PACKET_LENGTH).enc(sink)?; + u8::from(SftpNum::SSH_FXP_NAME).enc(sink)?; + req_id.enc(sink)?; + count.enc(sink)?; + + Ok(sink.payload_slice()) + } +} + +#[cfg(test)] +mod dir_reply_tests { + use super::*; + + #[cfg(test)] + extern crate std; + // #[cfg(test)] + // use std::println; + + #[test] + fn compose_header() { + const N: usize = 512; + + let req_id = ReqId(42); + let data_len = 128; + let count = 128; + let mut buffer = [0u8; N]; + let mut sink = SftpSink::new(&mut buffer); + + let payload = + DirReply::::encode_data_header(&mut sink, req_id, data_len, count) + .unwrap(); + + // println!("{payload:?}"); + + // println!("{:?}", &u32::from_be_bytes(payload[..4].try_into().unwrap())); + assert_eq!( + data_len + ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, + u32::from_be_bytes(payload[..4].try_into().unwrap()) + ); + } } pub mod helpers { @@ -347,7 +435,7 @@ pub mod helpers { /// as it would be serialized to the wire. /// /// Use this function to calculate the total length of a collection - /// of NameEntrys in order to send a correct response Name header + /// of `NameEntry`s in order to send a correct response Name header pub fn get_name_entry_len(name_entry: &NameEntry<'_>) -> SftpResult { let mut buf = [0u8; MAX_NAME_ENTRY_SIZE]; let mut temp_sink = SftpSink::new(&mut buf); From eeea557558e45de028cdec403c0197757dbd0586 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 25 Nov 2025 18:07:47 +1100 Subject: [PATCH 372/393] [skip ci] Clearing RequestHolder on reset() and updating field len on each SftpSink push call clearing the reset holder makes it easier to debug it finalizing every time data is serialized in the sink avoid usability by not relying on users finalizing the sink --- sftp/src/sftphandler/requestholder.rs | 3 ++- sftp/src/sftphandler/sftpoutputchannelhandler.rs | 1 - sftp/src/sftpsink.rs | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/sftp/src/sftphandler/requestholder.rs b/sftp/src/sftphandler/requestholder.rs index 1a02a381..3cb664e1 100644 --- a/sftp/src/sftphandler/requestholder.rs +++ b/sftp/src/sftphandler/requestholder.rs @@ -104,11 +104,12 @@ impl<'a> RequestHolder<'a> { /// /// Resets the `appended()` counter. /// - /// Will not clear the previous data from the buffer. + /// Will **clear** the previous data from the buffer. pub(crate) fn reset(&mut self) -> () { self.busy = false; self.buffer_fill_index = 0; self.appended = 0; + self.buffer.fill(0); } /// Appends a byte at a time to the internal buffer and tries to diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 29b611fe..9f32035f 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -138,7 +138,6 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { let mut sink = SftpSink::new(&mut buf); packet.encode_response(&mut sink)?; debug!("Output Producer: Sending packet {:?}", packet); - sink.finalize(); Self::send_buffer(&self.writer, &sink.used_slice(), &self.counter).await; Ok(()) } diff --git a/sftp/src/sftpsink.rs b/sftp/src/sftpsink.rs index 9fa221da..ad2ee18e 100644 --- a/sftp/src/sftpsink.rs +++ b/sftp/src/sftpsink.rs @@ -31,7 +31,7 @@ impl<'g> SftpSink<'g> { /// /// **Returns** the final index in the buffer as a reference of the /// space used - pub fn finalize(&mut self) -> usize { + fn finalize(&mut self) -> usize { if self.index <= SFTP_FIELD_LEN_LENGTH { warn!("SftpSink trying to terminate it before pushing data"); return 0; @@ -94,6 +94,7 @@ impl<'g> SSHSink for SftpSink<'g> { self.index += 1; }); trace!("Sink new index: {:}", self.index); + self.finalize(); Ok(()) } } From cae34f29d654ac08a419e903ce179b9ae99c3f51 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 25 Nov 2025 18:12:23 +1100 Subject: [PATCH 373/393] [skip ci] caught a condition where an unknown packet would not be flush --- sftp/src/sftphandler/sftphandler.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 0721d198..8d9ff23b 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -298,6 +298,10 @@ where "", ) .await?; + buf = &buf[buf.len() - source.remaining()..]; + debug!( + "Unknown Packet. clearing the buffer in place since it filts" + ); } Err(WireError::PacketWrong) => { error!("Not a request: "); @@ -369,7 +373,8 @@ where match request { // SftpPacket::Init(init_version_client) => todo!(), // SftpPacket::Version(init_version_lowest) => todo!(), - SftpPacket::Read(req_id, read) => { + SftpPacket::Read(req_id, ref read) => { + debug!("Read request: {:?}", request); if let Err(error) = self .file_server .read( From bf4e87e291462d8f0085b7b99c806a0d8561f347 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 25 Nov 2025 18:15:29 +1100 Subject: [PATCH 374/393] [skip ci] Checking the errors getting files Results of a simple test, repeated 10 times. Blocked receiving data, with our without error. Next I will add more visibility to the base sunset library --- demo/sftp/std/testing/out/test-get-ok.log | 64 +++++++++++++++++++ .../std/testing/out/test-get-problems.log | 29 +++++++++ 2 files changed, 93 insertions(+) create mode 100644 demo/sftp/std/testing/out/test-get-ok.log create mode 100644 demo/sftp/std/testing/out/test-get-problems.log diff --git a/demo/sftp/std/testing/out/test-get-ok.log b/demo/sftp/std/testing/out/test-get-ok.log new file mode 100644 index 00000000..7a34f5a4 --- /dev/null +++ b/demo/sftp/std/testing/out/test-get-ok.log @@ -0,0 +1,64 @@ +# Server + +[2025-11-25T06:50:18.099281543Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-25T06:50:18.099286120Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-25T06:50:18.099290277Z DEBUG sunset_sftp::sftphandler::sftphandler] source: SftpSource { buffer: [0, 0, 0, 25, 5, 0, 0, 0, 95, 0, 0, 0, 4, 250, 89, 118, 250, 0, 0, 0, 0, 0, 31, 128, 0, 0, 0, 128, 0], index: 0 } +[2025-11-25T06:50:18.099298850Z DEBUG sunset_sftp::sftphandler::sftphandler] Got a valid request SSH_FXP_READ +[2025-11-25T06:50:18.099303697Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: ProcessRequest { sftp_num: SSH_FXP_READ } ]=======================> Buffer remaining: 0 +[2025-11-25T06:50:18.099308605Z DEBUG sunset_sftp::sftphandler::sftphandler] Read request: Read(ReqId(95), Read { handle: FileHandle(BinString(len=4)), offset: 2064384, len: 32768 }) +[2025-11-25T06:50:18.105182953Z DEBUG sunset_sftp::sftphandler::sftphandler] Whole buffer processed. Getting more data +[2025-11-25T06:50:18.105227743Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes +[2025-11-25T06:50:18.105236126Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 +[2025-11-25T06:50:18.105241014Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 +[2025-11-25T06:50:18.105245260Z DEBUG sunset_sftp::sftphandler::sftphandler] source: SftpSource { buffer: [0, 0, 0, 25, 5, 0, 0, 0, 96, 0, 0, 0, 4, 250, 89, 118, 250, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 128, 0], index: 0 } +[2025-11-25T06:50:18.105254224Z DEBUG sunset_sftp::sftphandler::sftphandler] Got a valid request SSH_FXP_READ +[2025-11-25T06:50:18.105259022Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: ProcessRequest { sftp_num: SSH_FXP_READ } ]=======================> Buffer remaining: 0 +[2025-11-25T06:50:18.105287155Z DEBUG sunset_sftp::sftphandler::sftphandler] Read request: Read(ReqId(96), Read { handle: FileHandle(BinString(len=4)), offset: 2097152, len: 32768 }) +[2025-11-25T06:50:18.105326967Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF for "./demo/sftp/std/testing/out/./2048kB_random" +[2025-11-25T06:50:18.105355682Z DEBUG sunset_sftp::sftphandler::sftphandler] Whole buffer processed. Getting more data +[2025-11-25T06:50:18.106505167Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes +[2025-11-25T06:50:18.106547733Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 +[2025-11-25T06:50:18.106556397Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 +[2025-11-25T06:50:18.106560383Z DEBUG sunset_sftp::sftphandler::sftphandler] source: SftpSource { buffer: [0, 0, 0, 13, 4, 0, 0, 0, 97, 0, 0, 0, 4, 250, 89, 118, 250], index: 0 } +[2025-11-25T06:50:18.106568636Z DEBUG sunset_sftp::sftphandler::sftphandler] Got a valid request SSH_FXP_CLOSE +[2025-11-25T06:50:18.106573513Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: ProcessRequest { sftp_num: SSH_FXP_CLOSE } ]=======================> Buffer remaining: 0 +[2025-11-25T06:50:18.106612153Z DEBUG sunset_sftp::sftphandler::sftphandler] Whole buffer processed. Getting more data +[2025-11-25T06:50:18.107404343Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 0 bytes +[2025-11-25T06:50:18.107449914Z DEBUG sunset_sftp::sftphandler::sftphandler] client disconnected +[2025-11-25T06:50:18.107457566Z ERROR sunset_sftp::sftphandler::sftphandler] Processing returned: Err(ClientDisconnected) +[2025-11-25T06:50:18.107464357Z WARN sunset_demo_sftp_std] sftp_loop finished: Err(ChannelEOF) +[2025-11-25T06:50:18.107471177Z WARN sunset_demo_common::server] Ended with error ChannelEOF +[2025-11-25T06:50:18.107550140Z INFO sunset_demo_common::server] Listening on TCP:22... + + +# client + +debug3: Received data 2031616 -> 2064383 +debug3: Finish at 2129920 ( 2) +debug3: Received reply T:103 I:95 R:1 +debug3: Received data 2064384 -> 2097151 +debug3: Finish at 2129920 ( 1) +debug3: Received reply T:101 I:96 R:1 +2048kB_random 100% 2048KB 260.3KB/s 00:07 +debug3: Sent message SSH2_FXP_CLOSE I:97 +debug2: channel 0: rcvd adjust 519 +debug3: SSH2_FXP_STATUS 0 +sftp> +sftp> bye +debug2: channel 0: read failed rfd 4 maxlen 2036: Broken pipe +debug2: channel 0: read failed +debug2: chan_shutdown_read: channel 0: (i0 o0 sock -1 wfd 4 efd 6 [write]) +debug2: channel 0: input open -> drain +debug2: channel 0: ibuf empty +debug2: channel 0: send eof +debug3: send packet: type 96 +debug2: channel 0: input drain -> closed +debug3: send packet: type 1 +client_loop: send disconnect: Broken pipe +DOWNLOAD Test Results: +============= +Download PASS: 512B_random. Cleaning it +Download PASS: 16kB_random. Cleaning it +Download PASS: 64kB_random. Cleaning it +Download PASS: 65kB_random. Cleaning it +Download PASS: 2048kB_random. Cleaning it \ No newline at end of file diff --git a/demo/sftp/std/testing/out/test-get-problems.log b/demo/sftp/std/testing/out/test-get-problems.log new file mode 100644 index 00000000..8817e247 --- /dev/null +++ b/demo/sftp/std/testing/out/test-get-problems.log @@ -0,0 +1,29 @@ +# Context + +- run `cargo run -p sunset-demo-sftp-std` +- while running, used `test_get.sh` and `test_get_short.sh` to test downloads with random filled files. + +## Error list test_get.sh + +runs 10 + + - Pass (2/runs) + + - locked at (7/runs): +> [2025-11-25T06:46:50.719754226Z DEBUG sunset_sftp::sftphandler::sftphandler] Read request: Read(ReqId(96), Read { handle: FileHandle(BinString(len=4)), offset: 2097152, len: 32768 }) +> [2025-11-25T06:46:50.719770035Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF for "./demo/sftp/std/testing/out/./2048kB_random" +> [2025-11-25T06:46:50.719776688Z DEBUG sunset_sftp::sftphandler::sftphandler] Whole buffer processed. Getting more data + + - SSH channel full (1/runs): +> [2025-11-25T06:48:19.640903103Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 24 +> [2025-11-25T06:48:19.640941702Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 24 +> [2025-11-25T06:48:19.640971491Z DEBUG sunset_sftp::sftphandler::sftphandler] source: SftpSource { buffer: [0, 0, 0, 25, 5, 0, 0, 0, 93, 0, 0, 0, 4, 250, 89, 118, 250, 0, 0, 0, 0, 0, 30, 128], index: 0 } +> [2025-11-25T06:48:19.641007994Z DEBUG sunset_sftp::sftphandler::sftphandler] source: SftpSource { buffer: [0, 0, 0, 25, 5, 0, 0, 0, 93, 0, 0, 0, 4, 250, 89, 118, 250, 0, 0, 0, 0, 0, 30, 128], index: 17 } +> [2025-11-25T06:48:19.641043171Z DEBUG sunset_sftp::sftphandler::sftphandler] Incomplete packet. request holder initialized with 24 bytes +> [2025-11-25T06:48:19.641054923Z DEBUG sunset_sftp::sftphandler::sftphandler] Whole buffer processed. Getting more data +> [2025-11-25T06:48:19.641083595Z ERROR sunset_sftp::sftphandler::sftphandler] SSH channel is full + + +## Error list test_get.sh + +Todo \ No newline at end of file From 488d3d79c72750a00be893b3b064d701411e0e97 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 28 Nov 2025 15:53:20 +1100 Subject: [PATCH 375/393] [skip ci] Fixed test to accommodate that sinks don't need to be finalized --- sftp/src/proto.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 2bb6045a..7c4578b8 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -995,7 +995,6 @@ mod proto_tests { ]; let _ = status_packet.encode_response(&mut sink); - sink.finalize(); assert_eq!(&expected_status_packet_slice, sink.used_slice()); } From 5d1c1f9424aa4ec5eb00450e4c21e2dde96eaaf9 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 28 Nov 2025 16:12:58 +1100 Subject: [PATCH 376/393] [skip ci] Adding checks around sftpserver read and readdir Since in production the library will not be in control of the way the SftpServer trait implementer will do with the {Dir/Data}Reply parameters checking that the data announced to be sent and the data really sent would prevent running into invalid packet. Until further changes data length sent != data length announced will trigger a runtime bug. The trait documentation has been updated accordingly --- demo/sftp/std/src/demosftpserver.rs | 6 +- demo/sftp/std/src/main.rs | 13 +-- sftp/src/proto.rs | 2 +- sftp/src/sftphandler/sftphandler.rs | 55 +++++++++- .../sftphandler/sftpoutputchannelhandler.rs | 2 +- sftp/src/sftpserver.rs | 102 ++++++++++++------ 6 files changed, 134 insertions(+), 46 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index c44b34b2..d7fcc6f1 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -228,7 +228,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { opaque_file_handle: &DemoOpaqueFileHandle, offset: u64, len: u32, - reply: &ReadReply<'_, N>, + reply: &mut ReadReply<'_, N>, ) -> SftpResult<()> { if let PrivatePathHandle::File(private_file_handle) = self .handles_manager @@ -369,7 +369,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { async fn readdir( &mut self, opaque_dir_handle: &DemoOpaqueFileHandle, - reply: &DirReply<'_, N>, + reply: &mut DirReply<'_, N>, ) -> SftpOpResult<()> { debug!("read dir for {:?}", opaque_dir_handle); @@ -402,7 +402,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { let name_entry_collection = DirEntriesCollection::new(dir_iterator); let response_read_status = - name_entry_collection.send_response(reply).await?; + name_entry_collection.send_response( reply).await?; dir.read_status = response_read_status; return Ok(()); diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 2694d25e..40d0697b 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -226,20 +226,21 @@ async fn main(spawner: Spawner) { log::LevelFilter::Debug, ) // .filter_module( - // "sunset_sftp::requestholder", - // log::LevelFilter::Debug, - // ) - // .filter_module( - // "sunset_demo_sftp_std::demosftpserver", + // "sunset_sftp::sftphandler::requestholder", // log::LevelFilter::Debug, // ) + .filter_module( + "sunset_demo_sftp_std::demosftpserver", + log::LevelFilter::Debug, + ) // .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) // .filter_module("sunset_sftp", log::LevelFilter::Trace) // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Debug) // .filter_module( // "sunset_sftp::sftphandler::sftpoutputchannelhandler", - // log::LevelFilter::Info, + // log::LevelFilter::Debug, // ) + // // .filter_module("sunset::channel", log::LevelFilter::Trace) // .filter_module("sunset_async::async_sunset", log::LevelFilter::Trace) // .filter_module("sunset::runner", log::LevelFilter::Debug) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 7c4578b8..d1ebeb5b 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -288,7 +288,7 @@ pub struct Data<'a> { /// encoded [`SftpPacket::Data`] variant /// /// See [Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4) -pub(crate) const ENCODED_BASE_DATA_SFTP_PACKET_LENGTH: u32 = 1 + 4 + 4; +pub(crate) const ENCODED_SSH_FXP_DATA_MIN_LENGTH: u32 = 1 + 4 + 4; /// Struct to hold `SSH_FXP_NAME` response. /// See [SSH_FXP_NAME in Responses from the Server to the Client](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7) diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 8d9ff23b..dc9e25d2 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -176,7 +176,7 @@ where "", ) .await?; - debug!("Still in buffer: {buf:?}"); + trace!("Still in buffer: {buf:?}"); self.state = HandlerState::Idle; } else { self.state = @@ -255,7 +255,7 @@ where self.request_holder.reset(); debug!("Creating a source: buf_len = {:?}", buf.len()); let mut source = SftpSource::new(&buf); - debug!("source: {source:?}"); + trace!("source: {source:?}"); match SftpPacket::decode_request(&mut source) { Ok(request) => { @@ -375,13 +375,15 @@ where // SftpPacket::Version(init_version_lowest) => todo!(), SftpPacket::Read(req_id, ref read) => { debug!("Read request: {:?}", request); + + let mut reply = ReadReply::new(req_id, output_producer); if let Err(error) = self .file_server .read( &T::try_from(&read.handle)?, read.offset, read.len, - &ReadReply::new(req_id, output_producer), + &mut reply, ) .await { @@ -405,6 +407,25 @@ where .await?; } }; + + match reply.read_diff() { + diff if diff > 0 => { + debug!( + "ReadReply not completed after read operation. Still need to send {} bytes", + diff + ); + return Err(SunsetError::Bug.into()); + } + diff if diff < 0 => { + error!( + "ReadReply has sent more data than announced: {} bytes extra", + -diff + ); + return Err(SunsetError::Bug.into()); + } + _ => {} + } + self.state = HandlerState::Idle; } SftpPacket::LStat(req_id, LStat { file_path: path }) => { @@ -468,11 +489,12 @@ where self.state = HandlerState::Idle; } SftpPacket::ReadDir(req_id, read_dir) => { + let mut reply = DirReply::new(req_id, output_producer); if let Err(status) = self .file_server .readdir( &T::try_from(&read_dir.handle)?, - &DirReply::new(req_id, output_producer), + &mut reply, ) .await { @@ -486,6 +508,23 @@ where ) .await?; }; + match reply.read_diff() { + diff if diff > 0 => { + debug!( + "DirReply not completed after read operation. Still need to send {} bytes", + diff + ); + return Err(SunsetError::Bug.into()); + } + diff if diff < 0 => { + error!( + "DirReply has sent more data than announced: {} bytes extra", + -diff + ); + return Err(SunsetError::Bug.into()); + } + _ => {} + } self.state = HandlerState::Idle; } SftpPacket::OpenDir(req_id, open_dir) => { @@ -588,7 +627,7 @@ where .realpath(path_info.path.as_str()?) { Ok(name_entry) => { - let dir_reply = + let mut dir_reply = DirReply::new(req_id, output_producer); let encoded_len = crate::sftpserver::helpers::get_name_entry_len(&name_entry)?; @@ -604,6 +643,12 @@ where .send_header(1, encoded_len) .await?; dir_reply.send_item(&name_entry).await?; + if dir_reply.read_diff() != 0 { + error!( + "PathInfo reply not completed after sending the only item" + ); + return Err(SunsetError::Bug.into()); + } } Err(code) => { output_producer diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 9f32035f..227897ae 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -67,7 +67,7 @@ impl SftpOutputPipe { /// Consumer that takes ownership of [`ChanOut`]. It pipes the data received /// from a [`PipeReader`] into the channel -pub struct SftpOutputConsumer<'a, const N: usize> { +pub(crate) struct SftpOutputConsumer<'a, const N: usize> { reader: PipeReader<'a, SunsetRawMutex, N>, ssh_chan_out: ChanOut<'a>, counter: &'a CounterMutex, diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 133794cc..2d511f27 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,6 +1,6 @@ use crate::error::{SftpError, SftpResult}; use crate::proto::{ - ENCODED_BASE_DATA_SFTP_PACKET_LENGTH, ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, + ENCODED_SSH_FXP_DATA_MIN_LENGTH, ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, MAX_NAME_ENTRY_SIZE, NameEntry, PFlags, SftpNum, }; use crate::server::SftpSink; @@ -67,13 +67,28 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } /// Reads from a file that has previously being opened for reading + /// + /// ## Notes to the implementer: + /// + /// The implementer is expected to use the parameter `reply` [`DirReply`] to: + /// + /// - In case of no more data is to be sent, call `reply.send_eof()` + /// - There is more data to be sent from an open file: + /// 1. Call `reply.send_header()` with the length of data to be sent + /// 2. Call `reply.send_data()` once or multiple times to send all the data announced + /// 3. Do not call `reply.send_eof()` during this [`readdir`] method call + /// + + /// If the length communicated in the header does not match the total length of the data + /// sent using `reply.send_data()`, the SFTP session will be broken. + /// #[allow(unused)] async fn read( &mut self, opaque_file_handle: &T, offset: u64, len: u32, - reply: &ReadReply<'_, N>, + reply: &mut ReadReply<'_, N>, ) -> SftpResult<()> { log::error!( "SftpServer Read operation not defined: handle = {:?}, offset = {:?}, len = {:?}", @@ -119,6 +134,10 @@ where /// 2. Call `reply.send_item()` for each of the items announced to be sent /// 3. Do not call `reply.send_eof()` during this [`readdir`] method call /// + /// If the length communicated in the header does not match the total length of all + /// the items sent using `reply.send_item()`, the SFTP session will be + /// broken. + /// /// The server is expected to keep track of the number of items that remain to be sent /// to the client since the client will only stop asking for more elements in the /// directory when a read dir request is answer with an reply.send_eof() @@ -127,7 +146,7 @@ where async fn readdir( &mut self, opaque_dir_handle: &T, - reply: &DirReply<'_, N>, + reply: &mut DirReply<'_, N>, ) -> SftpOpResult<()> { log::error!( "SftpServer ReadDir operation not defined: handle = {:?}", @@ -176,6 +195,10 @@ pub struct ReadReply<'g, const N: usize> { /// Immutable writer chan_out: &'g SftpOutputProducer<'g, N>, + /// Length of data to be sent as announced in [`ReadReply::send_header`] + data_len: u32, + /// Length of data sent so far using [`ReadReply::send_data`] + data_sent_len: u32, } impl<'g, const N: usize> ReadReply<'g, N> { @@ -185,7 +208,7 @@ impl<'g, const N: usize> ReadReply<'g, N> { req_id: ReqId, chan_out: &'g SftpOutputProducer<'g, N>, ) -> Self { - ReadReply { req_id, chan_out } + ReadReply { req_id, chan_out, data_len:0, data_sent_len:0 } } // TODO Make this enforceable @@ -195,7 +218,7 @@ impl<'g, const N: usize> ReadReply<'g, N> { /// /// The packet data content, excluding the length must be sent using /// [`ReadReply::send_data`] - pub async fn send_header(&self, data_len: u32) -> SftpResult<()> { + pub async fn send_header(&mut self, data_len: u32) -> SftpResult<()> { debug!( "ReadReply: Sending header for request id {:?}: data length = {:?}", self.req_id, data_len @@ -206,7 +229,6 @@ impl<'g, const N: usize> ReadReply<'g, N> { let payload = ReadReply::::encode_data_header(&mut sink, self.req_id, data_len)?; - // let payload = sink.payload_slice(); debug!( "Sending header: len = {:?}, content = {:?}", payload.len(), @@ -214,6 +236,7 @@ impl<'g, const N: usize> ReadReply<'g, N> { ); // Sending payload_slice since we are not making use of the sink sftpPacket length calculation self.chan_out.send_data(payload).await?; + self.data_len = data_len; Ok(()) } @@ -221,8 +244,10 @@ impl<'g, const N: usize> ReadReply<'g, N> { /// the announced data length /// /// **Important**: Call this after you have called `send_header` - pub async fn send_data(&self, buff: &[u8]) -> SftpResult<()> { - self.chan_out.send_data(buff).await + pub async fn send_data(&mut self, buff: &[u8]) -> SftpResult<()> { + self.chan_out.send_data(buff).await?; + self.data_sent_len += buff.len() as u32; + Ok(()) } /// Sends EOF meaning that there is no more data to be sent @@ -231,15 +256,13 @@ impl<'g, const N: usize> ReadReply<'g, N> { self.chan_out.send_status(self.req_id, StatusCode::SSH_FX_EOF, "").await } - /// Sends EOF with request id increased by one - /// - /// **Warning**: This is an experimental patch to resolve the situations where a response gets - /// stuck - /// - pub async fn send_eof_plus_one(&self) -> SftpResult<()> { - self.chan_out - .send_status(ReqId(self.req_id.0 + 1), StatusCode::SSH_FX_EOF, "") - .await + /// Indicates whether all the data announced in the header has been sent + /// + /// returns 0 when all data has been sent + /// returns >0 when there is still data to be sent + /// returns <0 when too much data has been sent + pub fn read_diff(&self) -> i32 { + (self.data_len as i32) - (self.data_sent_len as i32) } fn encode_data_header( @@ -247,9 +270,14 @@ impl<'g, const N: usize> ReadReply<'g, N> { req_id: ReqId, data_len: u32, ) -> Result<&'g [u8], SftpError> { - (data_len + ENCODED_BASE_DATA_SFTP_PACKET_LENGTH).enc(sink)?; + + // length field + (data_len + ENCODED_SSH_FXP_DATA_MIN_LENGTH).enc(sink)?; + // packet type (1) u8::from(SftpNum::SSH_FXP_DATA).enc(sink)?; + // request id (4) req_id.enc(sink)?; + // data length (4) data_len.enc(sink)?; Ok(sink.payload_slice()) } @@ -276,14 +304,12 @@ mod read_reply_tests { let payload = ReadReply::::encode_data_header(&mut sink, req_id, data_len).unwrap(); - // println!("{payload:?}"); - - // println!("{:?}", &u32::from_be_bytes(payload[..4].try_into().unwrap())); assert_eq!( - data_len + ENCODED_BASE_DATA_SFTP_PACKET_LENGTH, + data_len + ENCODED_SSH_FXP_DATA_MIN_LENGTH, u32::from_be_bytes(payload[..4].try_into().unwrap()) ); } + } /// Uses for [`DirReply`] to: @@ -302,9 +328,12 @@ mod read_reply_tests { pub struct DirReply<'g, const N: usize> { /// The request Id that will be use`d in the response req_id: ReqId, - /// Immutable writer chan_out: &'g SftpOutputProducer<'g, N>, + /// Length of data to be sent as announced in [`ReadReply::send_header`] + data_len: u32, + /// Length of data sent so far using [`ReadReply::send_data`] + data_sent_len: u32, } impl<'g, const N: usize> DirReply<'g, N> { @@ -317,7 +346,7 @@ impl<'g, const N: usize> DirReply<'g, N> { chan_out: &'g SftpOutputProducer<'g, N>, ) -> Self { // DirReply { chan_out: chan_out_wrapper, req_id } - DirReply { req_id, chan_out } + DirReply { req_id, chan_out, data_len:0, data_sent_len:0 } } // TODO Make this enforceable @@ -325,7 +354,7 @@ impl<'g, const N: usize> DirReply<'g, N> { /// Sends the header to the client with the number of files as [`NameEntry`] and the [`SSHEncode`] /// length of all these [`NameEntry`] items pub async fn send_header( - &self, + &mut self, count: u32, items_encoded_len: u32, ) -> SftpResult<()> { @@ -349,13 +378,14 @@ impl<'g, const N: usize> DirReply<'g, N> { payload ); self.chan_out.send_data(payload).await?; + self.data_len = items_encoded_len; Ok(()) } /// Sends a directory item to the client as a [`NameEntry`] /// /// Call this - pub async fn send_item(&self, name_entry: &NameEntry<'_>) -> SftpResult<()> { + pub async fn send_item(&mut self, name_entry: &NameEntry<'_>) -> SftpResult<()> { let mut buffer = [0u8; MAX_NAME_ENTRY_SIZE]; let mut sftp_sink = SftpSink::new(&mut buffer); name_entry.enc(&mut sftp_sink).map_err(|err| { @@ -363,7 +393,9 @@ impl<'g, const N: usize> DirReply<'g, N> { StatusCode::SSH_FX_FAILURE })?; - self.chan_out.send_data(sftp_sink.payload_slice()).await + self.chan_out.send_data(sftp_sink.payload_slice()).await?; + self.data_sent_len += sftp_sink.payload_len() as u32; + Ok(()) } /// Sends EOF meaning that there is no more files in the directory @@ -371,6 +403,15 @@ impl<'g, const N: usize> DirReply<'g, N> { self.chan_out.send_status(self.req_id, StatusCode::SSH_FX_EOF, "").await } + /// Indicates whether all the data announced in the header has been sent + /// + /// returns 0 when all data has been sent + /// returns >0 when there is still data to be sent + /// returns <0 when too much data has been sent + pub fn read_diff(&self) -> i32 { + (self.data_len as i32) - (self.data_sent_len as i32) + } + fn encode_data_header( sink: &'g mut SftpSink<'g>, req_id: ReqId, @@ -387,6 +428,7 @@ impl<'g, const N: usize> DirReply<'g, N> { Ok(sink.payload_slice()) } + } #[cfg(test)] @@ -516,7 +558,7 @@ impl DirEntriesCollection { /// Returns a [`ReadStatus`] pub async fn send_response( &self, - reply: &DirReply<'_, N>, + reply: &mut DirReply<'_, N>, ) -> SftpOpResult { self.send_entries_header(reply).await?; self.send_entries(reply).await?; @@ -528,7 +570,7 @@ impl DirEntriesCollection { /// element async fn send_entries_header( &self, - reply: &DirReply<'_, N>, + reply: &mut DirReply<'_, N>, ) -> SftpOpResult<()> { reply.send_header(self.count, self.encoded_length).await.map_err(|e| { debug!("Could not send header {e:?}"); @@ -539,7 +581,7 @@ impl DirEntriesCollection { /// Sends the entries in the ReadDir iterator back to the client async fn send_entries( &self, - reply: &DirReply<'_, N>, + reply: &mut DirReply<'_, N>, ) -> SftpOpResult<()> { for entry in &self.entries { let filename = entry.file_name().to_string_lossy().into_owned(); From 17991f0c11fd320f0307767a8c8ede871b2d2d29 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Sun, 30 Nov 2025 12:25:35 +1100 Subject: [PATCH 377/393] [skip ci] some changes to the testing sh. -o LogLevel=[Error->Debug] --- demo/sftp/std/testing/test_get.sh | 5 ++++- demo/sftp/std/testing/test_get_single.sh | 2 +- demo/sftp/std/testing/test_long_write_requests.sh | 2 +- demo/sftp/std/testing/test_read_dir.sh | 2 +- demo/sftp/std/testing/test_stats.sh | 2 +- demo/sftp/std/testing/test_write_requests.sh | 2 +- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/demo/sftp/std/testing/test_get.sh b/demo/sftp/std/testing/test_get.sh index 73acd192..913e1741 100755 --- a/demo/sftp/std/testing/test_get.sh +++ b/demo/sftp/std/testing/test_get.sh @@ -29,9 +29,12 @@ for file in "${FILES[@]}"; do mv "./${file}" "./out/${file}" done +echo "Output folder content:" + +ls ./out -l echo "Downloading files..." -sftp -vvvvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +sftp -vvvvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=DEBUG ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'get ./%s\n' "${FILES[@]}") bye diff --git a/demo/sftp/std/testing/test_get_single.sh b/demo/sftp/std/testing/test_get_single.sh index 182c0eec..6df353fd 100755 --- a/demo/sftp/std/testing/test_get_single.sh +++ b/demo/sftp/std/testing/test_get_single.sh @@ -31,7 +31,7 @@ done echo "Downloading files..." -sftp -vvvvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +sftp -vvvvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=DEBUG ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'get ./%s\n' "${FILES[@]}") bye diff --git a/demo/sftp/std/testing/test_long_write_requests.sh b/demo/sftp/std/testing/test_long_write_requests.sh index 532edd32..71813ede 100755 --- a/demo/sftp/std/testing/test_long_write_requests.sh +++ b/demo/sftp/std/testing/test_long_write_requests.sh @@ -15,7 +15,7 @@ dd if=/dev/random bs=1048576 count=100 of=./100MB_random 2>/dev/null echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." # Upload all files -sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} -vvv << EOF +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=DEBUG ${REMOTE_USER}@${REMOTE_HOST} -vvv << EOF $(printf 'put ./%s\n' "${FILES[@]}") bye EOF diff --git a/demo/sftp/std/testing/test_read_dir.sh b/demo/sftp/std/testing/test_read_dir.sh index 29154da7..ec2f18d3 100755 --- a/demo/sftp/std/testing/test_read_dir.sh +++ b/demo/sftp/std/testing/test_read_dir.sh @@ -22,7 +22,7 @@ ls echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." # Upload all files -sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=DEBUG ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'put ./%s\n' "${FILES[@]}") ls -lh bye diff --git a/demo/sftp/std/testing/test_stats.sh b/demo/sftp/std/testing/test_stats.sh index f079fe3d..a5c2ceb5 100755 --- a/demo/sftp/std/testing/test_stats.sh +++ b/demo/sftp/std/testing/test_stats.sh @@ -16,7 +16,7 @@ dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." # Upload all files -sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +sftp -vvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=DEBUG ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'put ./%s\n' "${FILES[@]}") $(printf 'ls -lh ./%s\n' "${FILES[@]}") diff --git a/demo/sftp/std/testing/test_write_requests.sh b/demo/sftp/std/testing/test_write_requests.sh index 13a4dbcc..cabab6b2 100755 --- a/demo/sftp/std/testing/test_write_requests.sh +++ b/demo/sftp/std/testing/test_write_requests.sh @@ -21,7 +21,7 @@ dd if=/dev/random bs=1024 count=2048 of=./2048kB_random 2>/dev/null echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." # Upload all files -sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${REMOTE_USER}@${REMOTE_HOST} << EOF +sftp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=DEBUG ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'put ./%s\n' "${FILES[@]}") bye EOF From bd0d53c7726754ff948f859fab63a051644c2a57 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Sun, 30 Nov 2025 13:30:18 +1100 Subject: [PATCH 378/393] [skip ci] extra verbosity strace runs in sftp get tests I have rocorded packets (encrypted) and straces for exec and test script finding: - invalid packets reading from the channel - No data reading from channel after sending EOF - demo-sftp-std server stucked reading from file --- demo/sftp/std/src/main.rs | 21 ++++++++++----------- sftp/src/sftphandler/requestholder.rs | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 40d0697b..79bca8f9 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -223,23 +223,22 @@ async fn main(spawner: Spawner) { .filter_level(log::LevelFilter::Info) .filter_module( "sunset_sftp::sftphandler::sftphandler", - log::LevelFilter::Debug, + log::LevelFilter::Trace, + ) + .filter_module( + "sunset_sftp::sftphandler::requestholder", + log::LevelFilter::Trace, ) - // .filter_module( - // "sunset_sftp::sftphandler::requestholder", - // log::LevelFilter::Debug, - // ) .filter_module( "sunset_demo_sftp_std::demosftpserver", - log::LevelFilter::Debug, + log::LevelFilter::Trace, ) - // .filter_module("sunset_sftp::sftphandler", log::LevelFilter::Trace) // .filter_module("sunset_sftp", log::LevelFilter::Trace) // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Debug) - // .filter_module( - // "sunset_sftp::sftphandler::sftpoutputchannelhandler", - // log::LevelFilter::Debug, - // ) + .filter_module( + "sunset_sftp::sftphandler::sftpoutputchannelhandler", + log::LevelFilter::Trace, + ) // // .filter_module("sunset::channel", log::LevelFilter::Trace) // .filter_module("sunset_async::async_sunset", log::LevelFilter::Trace) diff --git a/sftp/src/sftphandler/requestholder.rs b/sftp/src/sftphandler/requestholder.rs index 3cb664e1..e8d3c939 100644 --- a/sftp/src/sftphandler/requestholder.rs +++ b/sftp/src/sftphandler/requestholder.rs @@ -171,7 +171,7 @@ impl<'a> RequestHolder<'a> { let mut source = SftpSource::new(self.try_get_ref()?); if let Ok(pt) = source.peak_packet_type() { if !pt.is_request() { - error!("The request candidate is not a request {pt:?}"); + error!("The request candidate is not a request: {pt:?}"); return Err(RequestHolderError::NotRequest); } } else { From a246e2bcde0a10707d89501e820723f21a6c99ce Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Mon, 1 Dec 2025 15:23:14 +1100 Subject: [PATCH 379/393] [skip ci] Starting logging stuck communication The next files have been updated to facilitate the collection of data. The data collected will include: - Output of trace level of the sunset-demo-sftp-std - strace for sunset-demo-sftp-std - Output for log_get_single.sh script --- demo/sftp/std/src/main.rs | 56 ++++++---------- demo/sftp/std/testing/log_get_single.sh | 67 +++++++++++++++++++ .../sftphandler/sftpoutputchannelhandler.rs | 5 +- 3 files changed, 90 insertions(+), 38 deletions(-) create mode 100755 demo/sftp/std/testing/log_get_single.sh diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 79bca8f9..6d4d29b7 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -219,43 +219,25 @@ async fn listen( #[embassy_executor::main] async fn main(spawner: Spawner) { - env_logger::builder() - .filter_level(log::LevelFilter::Info) - .filter_module( - "sunset_sftp::sftphandler::sftphandler", - log::LevelFilter::Trace, - ) - .filter_module( - "sunset_sftp::sftphandler::requestholder", - log::LevelFilter::Trace, - ) - .filter_module( - "sunset_demo_sftp_std::demosftpserver", - log::LevelFilter::Trace, - ) - // .filter_module("sunset_sftp", log::LevelFilter::Trace) - // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Debug) - .filter_module( - "sunset_sftp::sftphandler::sftpoutputchannelhandler", - log::LevelFilter::Trace, - ) - // - // .filter_module("sunset::channel", log::LevelFilter::Trace) - // .filter_module("sunset_async::async_sunset", log::LevelFilter::Trace) - // .filter_module("sunset::runner", log::LevelFilter::Debug) - // .filter_module("sunset::traffic", log::LevelFilter::Trace) - // .filter_module("sunset_sftp::sftpsink", log::LevelFilter::Info) - // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) - // .filter_module("sunset_sftp::sftpserver", log::LevelFilter::Info) - // .filter_module("sunset::runner", log::LevelFilter::Info) - // .filter_module("sunset::encrypt", log::LevelFilter::Info) - // .filter_module("sunset::conn", log::LevelFilter::Info) - // .filter_module("sunset::kex", log::LevelFilter::Info) - // .filter_module("async_io", log::LevelFilter::Info) - // .filter_module("polling", log::LevelFilter::Info) - // .filter_module("embassy_net", log::LevelFilter::Info) - .format_timestamp_nanos() - .init(); + let log_path = std::env::var("RUST_LOG_FILE").ok(); + + let mut builder = env_logger::Builder::new(); + + builder.filter_level(log::LevelFilter::Trace); + // if std::env::var("RUST_LOG").is_err() { + // } else { + // builder.filter_level(log::LevelFilter::Debug); + // } + builder.format_timestamp_nanos(); + + if let Some(path) = log_path { + let file = std::fs::File::create(path).expect("Failed to create log file"); + builder.target(env_logger::Target::Pipe(Box::new(file))); + } else { + builder.target(env_logger::Target::Stdout); + } + + builder.init(); spawner.spawn(main_task(spawner)).unwrap(); } diff --git a/demo/sftp/std/testing/log_get_single.sh b/demo/sftp/std/testing/log_get_single.sh new file mode 100755 index 00000000..e4f7886b --- /dev/null +++ b/demo/sftp/std/testing/log_get_single.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +echo "Launching latest build demo with log and strace..." +# Create logs directory if it doesn't exist +LOG_DIR="$PWD/logs" +mkdir -p "$LOG_DIR" + +CURRENT_DIR=$(pwd) +cd ../../../../ +RUST_LOG_FILE="$LOG_DIR/log_demo_get_single.log" RUST_LOG="trace" strace-opt $LOG_DIR target/debug/sunset-demo-sftp-std & +# Store the process ID + +echo "Sleeping for 3 seconds to let the server start..." +sleep 3 + +cd "$CURRENT_DIR" +echo "Testing Single long GETs..." + +echo "Cleaning up previous run files" +rm -f -r ./*_random ./out/*_random + + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + +# Define test files +# FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random" "2048kB_random") +FILES=("4096kB_random") + +echo "Generating random data files..." +# dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null +# dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null +# dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null +# dd if=/dev/random bs=1024 count=65 of=./65kB_random 2>/dev/null +# dd if=/dev/random bs=1024 count=256 of=./256kB_random 2>/dev/null +# dd if=/dev/random bs=1024 count=1024 of=./1024kB_random 2>/dev/null +dd if=/dev/random bs=1024 count=4096 of=./4096kB_random 2>/dev/null +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." + +echo "Moving to the server folder..." +for file in "${FILES[@]}"; do + mv "./${file}" "./out/${file}" +done + + +echo "Downloading files..." +sftp -vvvvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=DEBUG ${REMOTE_USER}@${REMOTE_HOST} << EOF +$(printf 'get ./%s\n' "${FILES[@]}") +bye +EOF + +echo "DOWNLOAD Test Results:" +echo "=============" +# Test each file +for file in "${FILES[@]}"; do + if diff "./${file}" "./out/${file}" >/dev/null 2>&1; then + echo "Download PASS: ${file}. Cleaning it" + rm -f -r ./${file} ./out/${file} + else + echo "Download FAIL: ${file}". Keeping for inspection + fi +done + +# At the end of your script, you might want to clean up: +echo "Tests complete, shutting down the server..." +pkill sunset-demo-sftp-std \ No newline at end of file diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 227897ae..85add884 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -165,10 +165,13 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { if buf.len() == 0 { break; } - trace!("Sending buffer {:?}", buf); let bytes_sent = writer.write(&buf).await; buf = &buf[bytes_sent..]; + trace!( + "Output Producer: sent {bytes_sent:?}. {:?} bytes remain ", + buf.len() + ); } } } From 6f6d1029dd54db73276cff75cf828e9e5b5bcb07 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 3 Dec 2025 15:30:58 +1100 Subject: [PATCH 380/393] [skip ci] Minor change regarding buffer names --- demo/sftp/std/src/main.rs | 4 ++-- sftp/src/sftphandler/sftphandler.rs | 15 +++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 6d4d29b7..fb02e438 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -163,7 +163,7 @@ impl DemoServer for StdDemo { // TODO Do some research to find reasonable default buffer lengths let mut buffer_in = [0u8; 512]; - let mut incomplete_request_buffer = [0u8; 512]; + let mut request_buffer = [0u8; 512]; match { let stdio = serv.stdio(ch).await?; @@ -173,7 +173,7 @@ impl DemoServer for StdDemo { SftpHandler::::new( &mut file_server, - &mut incomplete_request_buffer, + &mut request_buffer, ) .process_loop(stdio, &mut buffer_in) .await?; diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index dc9e25d2..15baab8b 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -102,15 +102,12 @@ where /// the request in the local system /// - `incomplete_request_buffer`: used to deal with fragmented /// packets during [`SftpHandler::process`] - pub fn new( - file_server: &'a mut S, - incomplete_request_buffer: &'a mut [u8], - ) -> Self { + pub fn new(file_server: &'a mut S, request_buffer: &'a mut [u8]) -> Self { SftpHandler { file_server, // partial_write_request_tracker: None, state: HandlerState::default(), - request_holder: RequestHolder::new(incomplete_request_buffer), + request_holder: RequestHolder::new(request_buffer), _marker: core::marker::PhantomData, } } @@ -376,7 +373,8 @@ where SftpPacket::Read(req_id, ref read) => { debug!("Read request: {:?}", request); - let mut reply = ReadReply::new(req_id, output_producer); + let mut reply = + ReadReply::new(req_id, output_producer); if let Err(error) = self .file_server .read( @@ -425,7 +423,7 @@ where } _ => {} } - + self.state = HandlerState::Idle; } SftpPacket::LStat(req_id, LStat { file_path: path }) => { @@ -489,7 +487,8 @@ where self.state = HandlerState::Idle; } SftpPacket::ReadDir(req_id, read_dir) => { - let mut reply = DirReply::new(req_id, output_producer); + let mut reply = + DirReply::new(req_id, output_producer); if let Err(status) = self .file_server .readdir( From 229c1b83f3491f7fe8daad054c7bb22c8aab9add Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 3 Dec 2025 17:02:31 +1100 Subject: [PATCH 381/393] [skip ci] After finally applying the suggested fix responses stall in 100MB get Will provide further logs in a comment. Improved log scripts. In summary, the communication stalls because the ChanOut.write at some point does not return after being called. The reception if packets does not show any anomaly. --- demo/sftp/std/src/demosftpserver.rs | 4 +- demo/sftp/std/testing/log_get_single.sh | 60 ++++--------------- .../{test_get_single.sh => test_get_long.sh} | 16 ++--- .../sftphandler/sftpoutputchannelhandler.rs | 1 + 4 files changed, 21 insertions(+), 60 deletions(-) rename demo/sftp/std/testing/{test_get_single.sh => test_get_long.sh} (62%) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index d7fcc6f1..57bbaada 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -306,7 +306,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { remaining = remaining.checked_sub(br).ok_or(StatusCode::SSH_FX_FAILURE)?; trace!( - "after substracting {} bytes, there are {} bytes remaining", + "after subtracting {} bytes, there are {} bytes remaining", br, remaining ); @@ -402,7 +402,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { let name_entry_collection = DirEntriesCollection::new(dir_iterator); let response_read_status = - name_entry_collection.send_response( reply).await?; + name_entry_collection.send_response(reply).await?; dir.read_status = response_read_status; return Ok(()); diff --git a/demo/sftp/std/testing/log_get_single.sh b/demo/sftp/std/testing/log_get_single.sh index e4f7886b..a3e4de3d 100755 --- a/demo/sftp/std/testing/log_get_single.sh +++ b/demo/sftp/std/testing/log_get_single.sh @@ -1,6 +1,14 @@ #!/bin/bash echo "Launching latest build demo with log and strace..." + +cargo build -p sunset-demo-sftp-std + +if [ $? -ne 0 ]; then + echo "Failed to build sunset-demo-sftp-std. Aborting" + return 1 +fi + # Create logs directory if it doesn't exist LOG_DIR="$PWD/logs" mkdir -p "$LOG_DIR" @@ -8,60 +16,16 @@ mkdir -p "$LOG_DIR" CURRENT_DIR=$(pwd) cd ../../../../ RUST_LOG_FILE="$LOG_DIR/log_demo_get_single.log" RUST_LOG="trace" strace-opt $LOG_DIR target/debug/sunset-demo-sftp-std & -# Store the process ID echo "Sleeping for 3 seconds to let the server start..." sleep 3 cd "$CURRENT_DIR" -echo "Testing Single long GETs..." +echo "Testing GET long file..." echo "Cleaning up previous run files" rm -f -r ./*_random ./out/*_random - -# Set remote server details -REMOTE_HOST="192.168.69.2" -REMOTE_USER="any" - -# Define test files -# FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random" "2048kB_random") -FILES=("4096kB_random") - -echo "Generating random data files..." -# dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null -# dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null -# dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null -# dd if=/dev/random bs=1024 count=65 of=./65kB_random 2>/dev/null -# dd if=/dev/random bs=1024 count=256 of=./256kB_random 2>/dev/null -# dd if=/dev/random bs=1024 count=1024 of=./1024kB_random 2>/dev/null -dd if=/dev/random bs=1024 count=4096 of=./4096kB_random 2>/dev/null -echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." - -echo "Moving to the server folder..." -for file in "${FILES[@]}"; do - mv "./${file}" "./out/${file}" -done - - -echo "Downloading files..." -sftp -vvvvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=DEBUG ${REMOTE_USER}@${REMOTE_HOST} << EOF -$(printf 'get ./%s\n' "${FILES[@]}") -bye -EOF - -echo "DOWNLOAD Test Results:" -echo "=============" -# Test each file -for file in "${FILES[@]}"; do - if diff "./${file}" "./out/${file}" >/dev/null 2>&1; then - echo "Download PASS: ${file}. Cleaning it" - rm -f -r ./${file} ./out/${file} - else - echo "Download FAIL: ${file}". Keeping for inspection - fi -done - -# At the end of your script, you might want to clean up: -echo "Tests complete, shutting down the server..." -pkill sunset-demo-sftp-std \ No newline at end of file +echo "Logging test_get_long... to $LOG_DIR" +./test_get_long.sh > $LOG_DIR/log_get_long.log 2>&1 +echo "Finished logging GET long file." \ No newline at end of file diff --git a/demo/sftp/std/testing/test_get_single.sh b/demo/sftp/std/testing/test_get_long.sh similarity index 62% rename from demo/sftp/std/testing/test_get_single.sh rename to demo/sftp/std/testing/test_get_long.sh index 6df353fd..dd034060 100755 --- a/demo/sftp/std/testing/test_get_single.sh +++ b/demo/sftp/std/testing/test_get_long.sh @@ -10,18 +10,15 @@ rm -f -r ./*_random ./out/*_random REMOTE_HOST="192.168.69.2" REMOTE_USER="any" + + +# Generate random data files +echo "Generating random data files..." # Define test files -# FILES=("512B_random" "16kB_random" "64kB_random" "65kB_random" "2048kB_random") -FILES=("4096kB_random") +FILES=("100MB_random") echo "Generating random data files..." -# dd if=/dev/random bs=512 count=1 of=./512B_random 2>/dev/null -# dd if=/dev/random bs=1024 count=16 of=./16kB_random 2>/dev/null -# dd if=/dev/random bs=1024 count=64 of=./64kB_random 2>/dev/null -# dd if=/dev/random bs=1024 count=65 of=./65kB_random 2>/dev/null -# dd if=/dev/random bs=1024 count=256 of=./256kB_random 2>/dev/null -# dd if=/dev/random bs=1024 count=1024 of=./1024kB_random 2>/dev/null -dd if=/dev/random bs=1024 count=4096 of=./4096kB_random 2>/dev/null +dd if=/dev/random bs=1048576 count=100 of=./100MB_random 2>/dev/null echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." echo "Moving to the server folder..." @@ -29,7 +26,6 @@ for file in "${FILES[@]}"; do mv "./${file}" "./out/${file}" done - echo "Downloading files..." sftp -vvvvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=DEBUG ${REMOTE_USER}@${REMOTE_HOST} << EOF $(printf 'get ./%s\n' "${FILES[@]}") diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 85add884..16766ca1 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -166,6 +166,7 @@ impl<'a, const N: usize> SftpOutputProducer<'a, N> { break; } + trace!("Output Producer: Tries to send {:?} bytes", buf.len()); let bytes_sent = writer.write(&buf).await; buf = &buf[bytes_sent..]; trace!( From 7e357204ebbc62464d30c77a5681f1fb31f20147 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 9 Dec 2025 17:03:36 +1100 Subject: [PATCH 382/393] [skip ci] Recording better logs Coordinating pcap files, test script output, demo program output and strace (only network traffic) --- demo/sftp/std/src/main.rs | 14 +-- .../std/testing/log_demo_sftp_with_test.sh | 95 +++++++++++++++++++ demo/sftp/std/testing/log_get_single.sh | 31 ------ demo/sftp/std/testing/log_get_single_long.sh | 1 + demo/sftp/std/testing/log_get_single_short.sh | 1 + demo/sftp/std/testing/test_get_long.sh | 3 +- demo/sftp/std/testing/test_get_short.sh | 45 +++++++++ 7 files changed, 151 insertions(+), 39 deletions(-) create mode 100755 demo/sftp/std/testing/log_demo_sftp_with_test.sh delete mode 100755 demo/sftp/std/testing/log_get_single.sh create mode 100755 demo/sftp/std/testing/log_get_single_long.sh create mode 100755 demo/sftp/std/testing/log_get_single_short.sh create mode 100755 demo/sftp/std/testing/test_get_short.sh diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index fb02e438..3486c9d8 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -230,12 +230,14 @@ async fn main(spawner: Spawner) { // } builder.format_timestamp_nanos(); - if let Some(path) = log_path { - let file = std::fs::File::create(path).expect("Failed to create log file"); - builder.target(env_logger::Target::Pipe(Box::new(file))); - } else { - builder.target(env_logger::Target::Stdout); - } + // if let Some(path) = log_path { + // let file = std::fs::File::create(path).expect("Failed to create log file"); + // builder.target(env_logger::Target::Pipe(Box::new(file))); + // } else { + // builder.target(env_logger::Target::Stdout); + // } + + builder.target(env_logger::Target::Stdout); builder.init(); diff --git a/demo/sftp/std/testing/log_demo_sftp_with_test.sh b/demo/sftp/std/testing/log_demo_sftp_with_test.sh new file mode 100755 index 00000000..20d73093 --- /dev/null +++ b/demo/sftp/std/testing/log_demo_sftp_with_test.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +TIME_STAMP=$(date +%Y%m%d_%H%M%S) +TEST_FILE=$1 +# Used for log files naming +BASE_NAME=$(basename "$TEST_FILE" | cut -d. -f1) +START_PWD=$PWD +PROYECT_ROOT=$(dirname "$PWD")/../../.. + +# Check if file exist and can be executed + +if [ ! -f "${TEST_FILE}" ]; then + echo "File ${TEST_FILE} not found" + exit 1 +fi +if [ ! -x "${TEST_FILE}" ]; then + echo "File ${TEST_FILE} is not executable" + exit 2 +fi + +echo "debuging file: $TEST_FILE with logging and pcap" + +cargo build -p sunset-demo-sftp-std +if [ $? -ne 0 ]; then + echo "Failed to build sunset-demo-sftp-std. Aborting" + return 1 +fi + +sleep 3; +clear; + +# Create logs directory if it doesn't exist +LOG_DIR="$PWD/logs" +mkdir -p "$LOG_DIR" + + +# Starts an Tshark session to capture packets in tap0 +WIRESHARK_LOG=${LOG_DIR}/${TIME_STAMP}_${BASE_NAME}.pcap +tshark -i tap0 -w ${WIRESHARK_LOG} & +TSHARK_PID=$! + +# waits while tshark started writting to the file +echo "Waiting for tshark to start..." + +while [ ! -s "${WIRESHARK_LOG}" ]; do + sleep 1 +done +echo "Tshark has started." + +# ################################################################ +# Start the sunset-demo-sftp-std with strace +# ################################################################ +echo "Starting sunset-demo-sftp-std" +echo "Changing directory to Project root: ${PROYECT_ROOT}" +cd ${PROYECT_ROOT} +echo "Project root directory is: ${PWD}" +RUST_LOG_FILE="${LOG_DIR}/${TIME_STAMP}_${BASE_NAME}.log" +STRACE_LOG=${LOG_DIR}/${TIME_STAMP}_${BASE_NAME}_strace.log +STRACE_OPTIONS="-fintttCDTYyy -v" +STRACE_CMD="strace ${STRACE_OPTIONS} -o ${STRACE_LOG} -P /dev/net/tun ./target/debug/sunset-demo-sftp-std" + +echo "Running strace for sunset-demo-sftp-std:" +echo "TZ=UTC ${STRACE_CMD}" +TZ=UTC ${STRACE_CMD} 2>&1 > $RUST_LOG_FILE & +STRACE_PID=$! + +echo "Sleeping for 2 seconds to let the server start..." +sleep 2 + +echo "Changing back to the starting directory: $START_PWD" +cd $START_PWD + +echo "Cleaning up previous run files" +rm -f -r ./*_random ./out/*_random + +echo "Running ${TEST_FILE}. Logging all data to ${LOG_DIR} with prefix ${TIME_STAMP}." +${TEST_FILE} | awk '{ cmd = "date -u +\"[%Y-%m-%dT%H:%M:%S.%NZ]\""; cmd | getline timestamp; print timestamp, $0; close(cmd) }' > $LOG_DIR/${TIME_STAMP}_${BASE_NAME}_client.log 2>&1 & +TEST_FILE_PID=$! + +cleanup() { + echo "Cleaning up..." + kill -SIGTERM $TSHARK_PID + kill -SIGTERM $STRACE_PID + kill -SIGTERM $TEST_FILE_PID + echo "Cleanup done." +} + +trap cleanup SIGINT SIGTERM EXIT + +echo "If stuck use Ctrl+C to stop the script and cleanup." +wait "$TEST_FILE_PID" + +echo "Finished executing ${TEST_FILE}" + +cleanup diff --git a/demo/sftp/std/testing/log_get_single.sh b/demo/sftp/std/testing/log_get_single.sh deleted file mode 100755 index a3e4de3d..00000000 --- a/demo/sftp/std/testing/log_get_single.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -echo "Launching latest build demo with log and strace..." - -cargo build -p sunset-demo-sftp-std - -if [ $? -ne 0 ]; then - echo "Failed to build sunset-demo-sftp-std. Aborting" - return 1 -fi - -# Create logs directory if it doesn't exist -LOG_DIR="$PWD/logs" -mkdir -p "$LOG_DIR" - -CURRENT_DIR=$(pwd) -cd ../../../../ -RUST_LOG_FILE="$LOG_DIR/log_demo_get_single.log" RUST_LOG="trace" strace-opt $LOG_DIR target/debug/sunset-demo-sftp-std & - -echo "Sleeping for 3 seconds to let the server start..." -sleep 3 - -cd "$CURRENT_DIR" -echo "Testing GET long file..." - -echo "Cleaning up previous run files" -rm -f -r ./*_random ./out/*_random - -echo "Logging test_get_long... to $LOG_DIR" -./test_get_long.sh > $LOG_DIR/log_get_long.log 2>&1 -echo "Finished logging GET long file." \ No newline at end of file diff --git a/demo/sftp/std/testing/log_get_single_long.sh b/demo/sftp/std/testing/log_get_single_long.sh new file mode 100755 index 00000000..f01b186d --- /dev/null +++ b/demo/sftp/std/testing/log_get_single_long.sh @@ -0,0 +1 @@ +./log_demo_sftp_with_test.sh ./test_get_long.sh \ No newline at end of file diff --git a/demo/sftp/std/testing/log_get_single_short.sh b/demo/sftp/std/testing/log_get_single_short.sh new file mode 100755 index 00000000..58342c33 --- /dev/null +++ b/demo/sftp/std/testing/log_get_single_short.sh @@ -0,0 +1 @@ +./log_demo_sftp_with_test.sh ./test_get_short.sh \ No newline at end of file diff --git a/demo/sftp/std/testing/test_get_long.sh b/demo/sftp/std/testing/test_get_long.sh index dd034060..69c60a04 100755 --- a/demo/sftp/std/testing/test_get_long.sh +++ b/demo/sftp/std/testing/test_get_long.sh @@ -27,9 +27,8 @@ for file in "${FILES[@]}"; do done echo "Downloading files..." -sftp -vvvvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=DEBUG ${REMOTE_USER}@${REMOTE_HOST} << EOF +sftp -vvvvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=DEBUG ${REMOTE_USER}@${REMOTE_HOST} 2>&1 << EOF $(printf 'get ./%s\n' "${FILES[@]}") - bye EOF diff --git a/demo/sftp/std/testing/test_get_short.sh b/demo/sftp/std/testing/test_get_short.sh new file mode 100755 index 00000000..6d5b5799 --- /dev/null +++ b/demo/sftp/std/testing/test_get_short.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +echo "Testing Single long GETs..." + +echo "Cleaning up previous run files" +rm -f -r ./*_random ./out/*_random + + +# Set remote server details +REMOTE_HOST="192.168.69.2" +REMOTE_USER="any" + + + +# Generate random data files +echo "Generating random data files..." +# Define test files +FILES=("1MB_random") + +echo "Generating random data files..." +dd if=/dev/random bs=1048576 count=1 of=./1MB_random 2>/dev/null +echo "Uploading files to ${REMOTE_USER}@${REMOTE_HOST}..." + +echo "Moving to the server folder..." +for file in "${FILES[@]}"; do + mv "./${file}" "./out/${file}" +done + +echo "Downloading files..." +sftp -vvvvv -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=DEBUG ${REMOTE_USER}@${REMOTE_HOST} 2>&1 << EOF +$(printf 'get %s\n' "${FILES[@]}") +bye +EOF + +echo "DOWNLOAD Test Results:" +echo "=============" +# Test each file +for file in "${FILES[@]}"; do + if diff "./${file}" "./out/${file}" >/dev/null 2>&1; then + echo "Download PASS: ${file}. Cleaning it" + rm -f -r ./${file} ./out/${file} + else + echo "Download FAIL: ${file}". Keeping for inspection + fi +done From fdfda8a67ef9b48a1d66a4bbf205ce426f45fded Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 10 Dec 2025 17:10:43 +1100 Subject: [PATCH 383/393] [skip ci] Exploring logs: Found important issue in OutputChannelHandler Not all the data that the producer tries to send reach the consumer, that is the one that writes the data in the ChannelOut. This is likely to be the responsible for the stuck communication. Apart from that, I added more testing and fixed issue in Proto::Data (SSH_FXP_DATA). It had too many fields. More details to come --- demo/sftp/std/testing/extract_txrx.sh | 24 +++++ .../std/testing/log_demo_sftp_with_test.sh | 16 +++- demo/sftp/std/testing/out/test-get-ok.log | 64 -------------- .../std/testing/out/test-get-problems.log | 29 ------ sftp/Cargo.toml | 1 + sftp/src/bin/read_packets_from_file.rs | 88 +++++++++++++++++++ sftp/src/lib.rs | 6 ++ sftp/src/proto.rs | 78 +++++++++++++--- sftp/src/sftpsource.rs | 8 ++ 9 files changed, 204 insertions(+), 110 deletions(-) create mode 100755 demo/sftp/std/testing/extract_txrx.sh delete mode 100644 demo/sftp/std/testing/out/test-get-ok.log delete mode 100644 demo/sftp/std/testing/out/test-get-problems.log create mode 100644 sftp/src/bin/read_packets_from_file.rs diff --git a/demo/sftp/std/testing/extract_txrx.sh b/demo/sftp/std/testing/extract_txrx.sh new file mode 100755 index 00000000..7c439a1c --- /dev/null +++ b/demo/sftp/std/testing/extract_txrx.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Find all lines containing SFTP... OR Output Consumer... +cat $1 | \ +grep -E 'SFTP <---- received: \[|Output Consumer: Bytes written \[' | \ +sed 's/.*received: /c / ; s/.*written /s /' > ${1}.txrx +# Extract received lines. Remove brackets, spaces, +# and split by comma into new lines. Finally remove empty lines. + +# RX +cat $1 | \ +grep -E 'SFTP <---- received: \[' | \ +sed 's/.*received: //' | \ +sed 's/\[//; s/\]/,/' | \ +tr -d ' ' |tr ',' '\n'| \ +grep -v '^$' > ${1}.rx + +# TX +cat $1 | \ +grep -E 'Output Consumer: Bytes written \[' | \ +sed 's/.*written //' | \ +sed 's/\[//; s/\]/,/' | \ +tr -d ' ' |tr ',' '\n'| \ +grep -v '^$' > ${1}.tx \ No newline at end of file diff --git a/demo/sftp/std/testing/log_demo_sftp_with_test.sh b/demo/sftp/std/testing/log_demo_sftp_with_test.sh index 20d73093..546fe604 100755 --- a/demo/sftp/std/testing/log_demo_sftp_with_test.sh +++ b/demo/sftp/std/testing/log_demo_sftp_with_test.sh @@ -77,19 +77,27 @@ echo "Running ${TEST_FILE}. Logging all data to ${LOG_DIR} with prefix ${TIME_ST ${TEST_FILE} | awk '{ cmd = "date -u +\"[%Y-%m-%dT%H:%M:%S.%NZ]\""; cmd | getline timestamp; print timestamp, $0; close(cmd) }' > $LOG_DIR/${TIME_STAMP}_${BASE_NAME}_client.log 2>&1 & TEST_FILE_PID=$! +kill_test(){ + echo "traped signal, killing test file process ${TEST_FILE_PID}" + kill -SIGTERM $TEST_FILE_PID +} cleanup() { echo "Cleaning up..." + if kill -0 $TSHARK_PID 2>/dev/null; then + echo "Killing tshark process ${TSHARK_PID}" kill -SIGTERM $TSHARK_PID - kill -SIGTERM $STRACE_PID - kill -SIGTERM $TEST_FILE_PID + fi + if kill -0 $STRACE_PID 2>/dev/null; then + echo "Killing strace process ${STRACE_PID}" + kill -SIGTERM $STRACE_PID + fi echo "Cleanup done." } -trap cleanup SIGINT SIGTERM EXIT +trap kill_test SIGINT SIGTERM echo "If stuck use Ctrl+C to stop the script and cleanup." wait "$TEST_FILE_PID" - echo "Finished executing ${TEST_FILE}" cleanup diff --git a/demo/sftp/std/testing/out/test-get-ok.log b/demo/sftp/std/testing/out/test-get-ok.log deleted file mode 100644 index 7a34f5a4..00000000 --- a/demo/sftp/std/testing/out/test-get-ok.log +++ /dev/null @@ -1,64 +0,0 @@ -# Server - -[2025-11-25T06:50:18.099281543Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-25T06:50:18.099286120Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-25T06:50:18.099290277Z DEBUG sunset_sftp::sftphandler::sftphandler] source: SftpSource { buffer: [0, 0, 0, 25, 5, 0, 0, 0, 95, 0, 0, 0, 4, 250, 89, 118, 250, 0, 0, 0, 0, 0, 31, 128, 0, 0, 0, 128, 0], index: 0 } -[2025-11-25T06:50:18.099298850Z DEBUG sunset_sftp::sftphandler::sftphandler] Got a valid request SSH_FXP_READ -[2025-11-25T06:50:18.099303697Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: ProcessRequest { sftp_num: SSH_FXP_READ } ]=======================> Buffer remaining: 0 -[2025-11-25T06:50:18.099308605Z DEBUG sunset_sftp::sftphandler::sftphandler] Read request: Read(ReqId(95), Read { handle: FileHandle(BinString(len=4)), offset: 2064384, len: 32768 }) -[2025-11-25T06:50:18.105182953Z DEBUG sunset_sftp::sftphandler::sftphandler] Whole buffer processed. Getting more data -[2025-11-25T06:50:18.105227743Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 29 bytes -[2025-11-25T06:50:18.105236126Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 29 -[2025-11-25T06:50:18.105241014Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 29 -[2025-11-25T06:50:18.105245260Z DEBUG sunset_sftp::sftphandler::sftphandler] source: SftpSource { buffer: [0, 0, 0, 25, 5, 0, 0, 0, 96, 0, 0, 0, 4, 250, 89, 118, 250, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 128, 0], index: 0 } -[2025-11-25T06:50:18.105254224Z DEBUG sunset_sftp::sftphandler::sftphandler] Got a valid request SSH_FXP_READ -[2025-11-25T06:50:18.105259022Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: ProcessRequest { sftp_num: SSH_FXP_READ } ]=======================> Buffer remaining: 0 -[2025-11-25T06:50:18.105287155Z DEBUG sunset_sftp::sftphandler::sftphandler] Read request: Read(ReqId(96), Read { handle: FileHandle(BinString(len=4)), offset: 2097152, len: 32768 }) -[2025-11-25T06:50:18.105326967Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF for "./demo/sftp/std/testing/out/./2048kB_random" -[2025-11-25T06:50:18.105355682Z DEBUG sunset_sftp::sftphandler::sftphandler] Whole buffer processed. Getting more data -[2025-11-25T06:50:18.106505167Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 17 bytes -[2025-11-25T06:50:18.106547733Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 17 -[2025-11-25T06:50:18.106556397Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 17 -[2025-11-25T06:50:18.106560383Z DEBUG sunset_sftp::sftphandler::sftphandler] source: SftpSource { buffer: [0, 0, 0, 13, 4, 0, 0, 0, 97, 0, 0, 0, 4, 250, 89, 118, 250], index: 0 } -[2025-11-25T06:50:18.106568636Z DEBUG sunset_sftp::sftphandler::sftphandler] Got a valid request SSH_FXP_CLOSE -[2025-11-25T06:50:18.106573513Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: ProcessRequest { sftp_num: SSH_FXP_CLOSE } ]=======================> Buffer remaining: 0 -[2025-11-25T06:50:18.106612153Z DEBUG sunset_sftp::sftphandler::sftphandler] Whole buffer processed. Getting more data -[2025-11-25T06:50:18.107404343Z DEBUG sunset_sftp::sftphandler::sftphandler] SFTP <---- received: 0 bytes -[2025-11-25T06:50:18.107449914Z DEBUG sunset_sftp::sftphandler::sftphandler] client disconnected -[2025-11-25T06:50:18.107457566Z ERROR sunset_sftp::sftphandler::sftphandler] Processing returned: Err(ClientDisconnected) -[2025-11-25T06:50:18.107464357Z WARN sunset_demo_sftp_std] sftp_loop finished: Err(ChannelEOF) -[2025-11-25T06:50:18.107471177Z WARN sunset_demo_common::server] Ended with error ChannelEOF -[2025-11-25T06:50:18.107550140Z INFO sunset_demo_common::server] Listening on TCP:22... - - -# client - -debug3: Received data 2031616 -> 2064383 -debug3: Finish at 2129920 ( 2) -debug3: Received reply T:103 I:95 R:1 -debug3: Received data 2064384 -> 2097151 -debug3: Finish at 2129920 ( 1) -debug3: Received reply T:101 I:96 R:1 -2048kB_random 100% 2048KB 260.3KB/s 00:07 -debug3: Sent message SSH2_FXP_CLOSE I:97 -debug2: channel 0: rcvd adjust 519 -debug3: SSH2_FXP_STATUS 0 -sftp> -sftp> bye -debug2: channel 0: read failed rfd 4 maxlen 2036: Broken pipe -debug2: channel 0: read failed -debug2: chan_shutdown_read: channel 0: (i0 o0 sock -1 wfd 4 efd 6 [write]) -debug2: channel 0: input open -> drain -debug2: channel 0: ibuf empty -debug2: channel 0: send eof -debug3: send packet: type 96 -debug2: channel 0: input drain -> closed -debug3: send packet: type 1 -client_loop: send disconnect: Broken pipe -DOWNLOAD Test Results: -============= -Download PASS: 512B_random. Cleaning it -Download PASS: 16kB_random. Cleaning it -Download PASS: 64kB_random. Cleaning it -Download PASS: 65kB_random. Cleaning it -Download PASS: 2048kB_random. Cleaning it \ No newline at end of file diff --git a/demo/sftp/std/testing/out/test-get-problems.log b/demo/sftp/std/testing/out/test-get-problems.log deleted file mode 100644 index 8817e247..00000000 --- a/demo/sftp/std/testing/out/test-get-problems.log +++ /dev/null @@ -1,29 +0,0 @@ -# Context - -- run `cargo run -p sunset-demo-sftp-std` -- while running, used `test_get.sh` and `test_get_short.sh` to test downloads with random filled files. - -## Error list test_get.sh - -runs 10 - - - Pass (2/runs) - - - locked at (7/runs): -> [2025-11-25T06:46:50.719754226Z DEBUG sunset_sftp::sftphandler::sftphandler] Read request: Read(ReqId(96), Read { handle: FileHandle(BinString(len=4)), offset: 2097152, len: 32768 }) -> [2025-11-25T06:46:50.719770035Z INFO sunset_demo_sftp_std::demosftpserver] offset is larger than file length, sending EOF for "./demo/sftp/std/testing/out/./2048kB_random" -> [2025-11-25T06:46:50.719776688Z DEBUG sunset_sftp::sftphandler::sftphandler] Whole buffer processed. Getting more data - - - SSH channel full (1/runs): -> [2025-11-25T06:48:19.640903103Z DEBUG sunset_sftp::sftphandler::sftphandler] <=======================[ SFTP Process State: Idle ]=======================> Buffer remaining: 24 -> [2025-11-25T06:48:19.640941702Z DEBUG sunset_sftp::sftphandler::sftphandler] Creating a source: buf_len = 24 -> [2025-11-25T06:48:19.640971491Z DEBUG sunset_sftp::sftphandler::sftphandler] source: SftpSource { buffer: [0, 0, 0, 25, 5, 0, 0, 0, 93, 0, 0, 0, 4, 250, 89, 118, 250, 0, 0, 0, 0, 0, 30, 128], index: 0 } -> [2025-11-25T06:48:19.641007994Z DEBUG sunset_sftp::sftphandler::sftphandler] source: SftpSource { buffer: [0, 0, 0, 25, 5, 0, 0, 0, 93, 0, 0, 0, 4, 250, 89, 118, 250, 0, 0, 0, 0, 0, 30, 128], index: 17 } -> [2025-11-25T06:48:19.641043171Z DEBUG sunset_sftp::sftphandler::sftphandler] Incomplete packet. request holder initialized with 24 bytes -> [2025-11-25T06:48:19.641054923Z DEBUG sunset_sftp::sftphandler::sftphandler] Whole buffer processed. Getting more data -> [2025-11-25T06:48:19.641083595Z ERROR sunset_sftp::sftphandler::sftphandler] SSH channel is full - - -## Error list test_get.sh - -Todo \ No newline at end of file diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 704a9a84..f5904c19 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -18,6 +18,7 @@ sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } embedded-io-async = "0.6" num_enum = {version = "0.7.4"} paste = "1.0" +env_logger = "0.11" # Used for logging in binaries log = "0.4" embassy-sync = "0.7.2" embassy-futures = "0.1.2" diff --git a/sftp/src/bin/read_packets_from_file.rs b/sftp/src/bin/read_packets_from_file.rs new file mode 100644 index 00000000..b93960ec --- /dev/null +++ b/sftp/src/bin/read_packets_from_file.rs @@ -0,0 +1,88 @@ +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::env; +use env_logger; +use log::{error, info, warn}; +use sunset::ChanData; +use sunset_sftp::SftpSource; +use sunset_sftp::protocol::{NameEntry, SftpPacket}; +use sunset::sshwire::{SSHDecode, SSHSource}; +/// This program reads packets from a specified file and prints their byte representation. +/// +/// We assume that each line contains an u8. a way to get this format is to use the +/// `demo/sftp/std/testing/extract_txrx.sh` on a sunset-demo-sftp-std log file. +/// +/// # Usage +/// ``` +/// cargo run --package sunset-sftp --bin read_packets_from_file +/// ``` +/// where `` is the path to the file containing the packets. +/// +fn main() { + // env_logger::Builder::new() + // .filter_level(log::LevelFilter::Trace) + // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) + // .format_timestamp_nanos() + // .init(); + + let args: Vec = env::args().collect(); + + if args.len() < 2 { + eprintln!("Usage: {} ", args[0]); + std::process::exit(1); + } + + let file_path = &args[1]; + let file = File::open(file_path).expect("Failed to open file"); + let reader = BufReader::new(file); + let mut bytes: Vec = Vec::new(); + for (i,line) in reader.lines().enumerate() { + match line { + Ok(content) => { + let num = content.parse::().expect(format!("Failed to parse line {i} as u8").as_str()); + bytes.push(num); + } + Err(e) => eprintln!("Error reading line {}: {}",i, e), + } + } + println!("Read {} u8 elements from file {}", bytes.len(), file_path); + let mut used = 0; + let mut source = SftpSource::new(&bytes.as_slice()); + while source.remaining() > 0 { + match SftpPacket::decode(&mut source) { + Ok(packet) => { + + match packet { + SftpPacket::Name(req_id, name) => { + println!("SFTP Name: {:?} {:?}", req_id, name); + for i in 0..name.count { + println!("--({i}) Entry: {:?}", NameEntry::dec(&mut source).expect("Failed to decode NameEntry")); + } + } + SftpPacket::Handle(req_id, handle ) => { + println!("SFTP Handle: {:?} {:?}", req_id, handle); + } + SftpPacket::Attrs(req_id, attrs ) => { + println!("SFTP Attrs: {:?} {:?}", req_id, attrs); + } + SftpPacket::Data(req_id, data ) => { + println!("SFTP Data: {:?} {:?} bytes", req_id, data); + } + _ => { + println!("Decoded packet: {:?}", packet); + } + } + } + Err(e) => { + println!("Error decoding packet: {:?}. Up to: {:?}", e, source.buffer_used().len()); + + break; + } + } + let prev_used = used; + used = source.buffer_used().len(); + let last_used = used - prev_used; + println!("Last 9 bytes : {:?}, Lines {:?}-{used}, Counters: ({last_used}/{used}) [last/total decoded]\n", &source.buffer_used()[used-9..], prev_used+1 ); + } + // source.decode_all_packets().expect("Failed to decode packets"); +} \ No newline at end of file diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 4e3b2c0e..885de9c0 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -65,6 +65,11 @@ mod sftpsource; // Please see basic usage at `../demo/sftd/std` pub use sftphandler::SftpHandler; +/// Source of SFTP packets +/// +/// Used to decode SFTP packets from a byte slice +pub use sftpsource::SftpSource; + /// Structures and types used to add the details for the target system /// Related to the implementation of the [`server::SftpServer`], which /// is meant to be instantiated by the user and passed to [`SftpHandler`] @@ -99,6 +104,7 @@ pub mod handles { /// SFTP Protocol types and structures pub mod protocol { + pub use crate::proto::SftpPacket; pub use crate::proto::Attrs; pub use crate::proto::FileHandle; pub use crate::proto::Filename; diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index d1ebeb5b..c1bfad72 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -268,10 +268,6 @@ pub struct Handle<'a> { /// Used for `ssh_fxp_data` [responses](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7). #[derive(Debug, SSHEncode, SSHDecode)] pub struct Data<'a> { - /// Handle for the file referred - pub handle: FileHandle<'a>, - /// Offset in the data read - pub offset: u64, /// raw data pub data: BinString<'a>, } @@ -824,22 +820,28 @@ macro_rules! sftpmessages { /// Decode a response. /// /// Used by a SFTP client. Does not include the length field. - pub fn decode_response<'de>(s: &mut SftpSource<'de>) -> WireResult<(ReqId, Self)> + pub fn decode_response<'de>(s: &mut SftpSource<'de>) -> WireResult where // S: SftpSource<'de>, 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes 'de: 'a { - let num = SftpNum::from(u8::dec(s)?); + let packet_length = u32::dec(s)?; + trace!("Packet field len = {:?}, buffer len = {:?}", packet_length, s.remaining()); + match Self::dec(s) { + Ok(sftp_packet)=> { + if !sftp_packet.sftp_num().is_response() + { + Err(WireError::PacketWrong) + }else{ + Ok(sftp_packet) - if !num.is_response() { - return Err(WireError::PacketWrong) - // return error::SSHProto.fail(); - // Not an error in the SSHProtocol rather the SFTP Protocol. + } + }, + Err(e) => { + Err(e) + } } - - let id = ReqId(u32::dec(s)?); - Ok((id, Self::dec(s)?)) } @@ -878,6 +880,22 @@ macro_rules! sftpmessages { } } + /// Decode a a packet without checking if it is request or response + /// + /// Used by a SFTP server. Does not include the length field. + /// + /// It will fail if the received packet is a response, no valid or incomplete packet + pub fn decode<'de>(s: &mut SftpSource<'de>) -> WireResult + where + // S: SftpSource<'de>, + 'a: 'de, // 'a must outlive 'de and 'de must outlive 'a so they have matching lifetimes + 'de: 'a + { + let packet_length = u32::dec(s)?; + trace!("Packet field len = {:?}, buffer remaining = {:?}", packet_length, s.remaining()); + Self::dec(s) + } + // TODO Maybe change WireResult -> SftpResult and SSHSink to SftpSink? // This way I have more internal details and can return a Error::bug() if required /// Encode a response. @@ -970,6 +988,40 @@ mod proto_tests { #[cfg(test)] use std::println; + #[test] + fn test_data_roundtrip() { + let file_handle = b"handle123".as_slice(); + let data_slice = b"Hello, world!".as_slice(); + let mut buff = [0u8; 512]; + let data_packet = SftpPacket::Data( + ReqId(10), + Data { + handle: FileHandle{0: BinString(file_handle)}, + data: BinString(data_slice), + }, + ); + + let mut sink = SftpSink::new(&mut buff); + data_packet.encode_response(&mut sink).expect("Failed to encode response"); + println!( + "data_packet encoded_len = {:?}, encoded = {:?}", + sink.payload_len(), + sink.payload_slice() + ); + let mut source = SftpSource::new(sink.used_slice()); + println!("source = {:?}", source); + + match SftpPacket::decode_response(&mut source) { + Ok(SftpPacket::Data(req_id, data)) => { + assert_eq!(req_id, ReqId(10)); + assert_eq!(data.handle, FileHandle{0: BinString(file_handle)}); + assert_eq!(data.data, BinString(data_slice)); + } + Ok(other) => panic!("Expected Data packet, got: {:?}", other), + Err(e) => panic!("Failed to decode packet: {:?}", e), + } + } + #[test] fn test_status_encoding() { let mut buf = [0u8; 256]; diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 35b18ac6..774622d5 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -116,6 +116,14 @@ impl<'de> SftpSource<'de> { } } + /// Peaks the buffer for packet request id [`u32`]. This does not advance + /// the reading index + /// + /// Useful to observe the packet fields in special conditions where a + /// `dec(s)` would fail + /// + /// **Warning**: will only work in well formed packets, in other case + /// the result will contains garbage pub fn peak_packet_req_id(&self) -> WireResult { if self.buffer.len() < SFTP_FIELD_REQ_ID_INDEX + SFTP_FIELD_REQ_ID_LEN { Err(WireError::RanOut) From cb8fbdabdfc0b9d603c1717c62c7c9c919f00ac4 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 10 Dec 2025 17:55:45 +1100 Subject: [PATCH 384/393] [skip ci] Fixing test for Data roundtrip and updating Cargo.lock --- Cargo.lock | 2 ++ sftp/src/proto.rs | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0176d4c4..95a311fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1133,6 +1133,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" dependencies = [ "log", + "regex", ] [[package]] @@ -2920,6 +2921,7 @@ dependencies = [ "embassy-futures", "embassy-sync 0.7.2", "embedded-io-async", + "env_logger", "log", "num_enum 0.7.4", "paste", diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index c1bfad72..9a317595 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -990,13 +990,11 @@ mod proto_tests { #[test] fn test_data_roundtrip() { - let file_handle = b"handle123".as_slice(); let data_slice = b"Hello, world!".as_slice(); let mut buff = [0u8; 512]; let data_packet = SftpPacket::Data( ReqId(10), Data { - handle: FileHandle{0: BinString(file_handle)}, data: BinString(data_slice), }, ); @@ -1014,7 +1012,6 @@ mod proto_tests { match SftpPacket::decode_response(&mut source) { Ok(SftpPacket::Data(req_id, data)) => { assert_eq!(req_id, ReqId(10)); - assert_eq!(data.handle, FileHandle{0: BinString(file_handle)}); assert_eq!(data.data, BinString(data_slice)); } Ok(other) => panic!("Expected Data packet, got: {:?}", other), From e1148dd076fbd5180afabcf72d4458aa403dfa48 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 12 Dec 2025 10:14:45 +1100 Subject: [PATCH 385/393] [skip ci] Removed env-logger dependency for sftp It was introduced for read_packet_from_file.rs and it is no longer needed --- Cargo.lock | 2 -- sftp/Cargo.toml | 1 - sftp/src/bin/read_packets_from_file.rs | 10 +--------- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95a311fb..0176d4c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1133,7 +1133,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" dependencies = [ "log", - "regex", ] [[package]] @@ -2921,7 +2920,6 @@ dependencies = [ "embassy-futures", "embassy-sync 0.7.2", "embedded-io-async", - "env_logger", "log", "num_enum 0.7.4", "paste", diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index f5904c19..704a9a84 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -18,7 +18,6 @@ sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } embedded-io-async = "0.6" num_enum = {version = "0.7.4"} paste = "1.0" -env_logger = "0.11" # Used for logging in binaries log = "0.4" embassy-sync = "0.7.2" embassy-futures = "0.1.2" diff --git a/sftp/src/bin/read_packets_from_file.rs b/sftp/src/bin/read_packets_from_file.rs index b93960ec..37f9fb60 100644 --- a/sftp/src/bin/read_packets_from_file.rs +++ b/sftp/src/bin/read_packets_from_file.rs @@ -1,9 +1,7 @@ use std::fs::File; use std::io::{BufRead, BufReader}; use std::env; -use env_logger; -use log::{error, info, warn}; -use sunset::ChanData; + use sunset_sftp::SftpSource; use sunset_sftp::protocol::{NameEntry, SftpPacket}; use sunset::sshwire::{SSHDecode, SSHSource}; @@ -19,11 +17,6 @@ use sunset::sshwire::{SSHDecode, SSHSource}; /// where `` is the path to the file containing the packets. /// fn main() { - // env_logger::Builder::new() - // .filter_level(log::LevelFilter::Trace) - // .filter_module("sunset_sftp::sftpsource", log::LevelFilter::Info) - // .format_timestamp_nanos() - // .init(); let args: Vec = env::args().collect(); @@ -84,5 +77,4 @@ fn main() { let last_used = used - prev_used; println!("Last 9 bytes : {:?}, Lines {:?}-{used}, Counters: ({last_used}/{used}) [last/total decoded]\n", &source.buffer_used()[used-9..], prev_used+1 ); } - // source.decode_all_packets().expect("Failed to decode packets"); } \ No newline at end of file From 1760b63bb73c50a87d7009aa5229e5d004ee73f5 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 12 Dec 2025 10:26:56 +1100 Subject: [PATCH 386/393] [skip ci] Extracting sftpoutputproducer bytes for analysis At this point I can see that on stall, the backpressure from the ChanOut blocks the data put in the pipe. I took measures in a previous commit (5d1c1f9424aa4ec5eb00450e4c21e2dde96eaaf9) to guarantee that for SSH_FXP_DATA and SSH_FXP_NAME the announced length and real length matches. - SftpServer Unit tests to check the length in header and data sent - Runtime checks for ReadDir, Read and PathInfo requests I am going to start digging in the ChanOut Write process --- demo/sftp/std/testing/extract_txrx.sh | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/demo/sftp/std/testing/extract_txrx.sh b/demo/sftp/std/testing/extract_txrx.sh index 7c439a1c..f2c7ab82 100755 --- a/demo/sftp/std/testing/extract_txrx.sh +++ b/demo/sftp/std/testing/extract_txrx.sh @@ -1,9 +1,20 @@ #!/bin/bash -# Find all lines containing SFTP... OR Output Consumer... +# Find all lines containing SFTP... OR Output Consumer... OR Output Producer... +# and reformat them into a simpler form for further processing. + + +cat < ${1}.txrx +Extracting communications from sunset-demo-sftp-std log file: $1 +Extract of RX (c: Client), TX (s: server), And internal TX (p: pipe producer) +------------------------------------------------ +EOF + cat $1 | \ -grep -E 'SFTP <---- received: \[|Output Consumer: Bytes written \[' | \ -sed 's/.*received: /c / ; s/.*written /s /' > ${1}.txrx +grep -E 'SFTP <---- received: \[|Output Consumer: Bytes written \[|Output Producer: Sending buffer \[' | \ +sed 's/.*received: /c / ; s/.*written /s / ; s/.*Output Producer: Sending buffer /p /' >> ${1}.txrx + + # Extract received lines. Remove brackets, spaces, # and split by comma into new lines. Finally remove empty lines. @@ -15,6 +26,14 @@ sed 's/\[//; s/\]/,/' | \ tr -d ' ' |tr ',' '\n'| \ grep -v '^$' > ${1}.rx +# Producer +cat $1 | \ +grep -E 'Output Producer: Sending buffer \[' | \ +sed 's/.*buffer //' | \ +sed 's/\[//; s/\]/,/' | \ +tr -d ' ' |tr ',' '\n'| \ +grep -v '^$' > ${1}.txp + # TX cat $1 | \ grep -E 'Output Consumer: Bytes written \[' | \ From 3d5e1bba9bbb02b3e8168ca24ffc919eecb40d3c Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 12 Dec 2025 11:04:48 +1100 Subject: [PATCH 387/393] [skip ci] Replacing write_all with write loop with no change This gives us visibility on when the ChanOut won't take all the bytes. It hapens regularly and that is expected. Does not provide extra visibility on the problem. I will leave it like this for now --- .../sftphandler/sftpoutputchannelhandler.rs | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index 16766ca1..e298990a 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -76,6 +76,27 @@ pub(crate) struct SftpOutputConsumer<'a, const N: usize> { impl<'a, const N: usize> SftpOutputConsumer<'a, N> { /// Run it to start the piping pub async fn receive_task(&mut self) -> SftpResult<()> { + // TODO: Revert to the simpler version once the root cause of the stall is found + // debug!("Running SftpOutout Consumer Reader task"); + // let mut buf = [0u8; N]; + // loop { + // let rl = self.reader.read(&mut buf).await; + // let mut _total = 0; + // { + // let mut lock = self.counter.lock().await; + // *lock += rl; + // _total = *lock; + // } + + // debug!("Output Consumer: ---> Reads {rl} bytes. Total {_total}"); + // if rl > 0 { + // self.ssh_chan_out.write_all(&buf[..rl]).await?; + // debug!("Output Consumer: Written {:?} bytes ", &buf[..rl].len()); + // trace!("Output Consumer: Bytes written {:?}", &buf[..rl]); + // } else { + // error!("Output Consumer: Empty array received"); + // } + // } debug!("Running SftpOutout Consumer Reader task"); let mut buf = [0u8; N]; loop { @@ -88,10 +109,26 @@ impl<'a, const N: usize> SftpOutputConsumer<'a, N> { } debug!("Output Consumer: ---> Reads {rl} bytes. Total {_total}"); + let mut scanning_buffer = &buf[..rl]; if rl > 0 { - self.ssh_chan_out.write_all(&buf[..rl]).await?; - debug!("Output Consumer: Written {:?} bytes ", &buf[..rl].len()); - trace!("Output Consumer: Bytes written {:?}", &buf[..rl]); + // Replaced write_all with loop to handle partial writes to discard issues in write_all + while scanning_buffer.len() > 0 { + trace!( + "Output Consumer: Tries to write {:?} bytes to ChanOut", + scanning_buffer.len() + ); + let wl = self.ssh_chan_out.write(scanning_buffer).await?; + debug!("Output Consumer: Written {:?} bytes ", wl); + if wl< scanning_buffer.len() { + debug!("Output Consumer: ChanOut accepted only part of the buffer"); + } + trace!( + "Output Consumer: Bytes written {:?}", + &scanning_buffer[..wl] + ); + scanning_buffer = &scanning_buffer[wl..]; + } + debug!("Output Consumer: Finished writing all bytes in read buffer"); } else { error!("Output Consumer: Empty array received"); } From 937fcd35da211007f221cd7edb37471faa7f2ba2 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Tue, 16 Dec 2025 13:22:08 +1100 Subject: [PATCH 388/393] [skip ci] log_demo_sftp_with_tests: Adding automatic TX/RX data extraction --- demo/sftp/std/testing/log_demo_sftp_with_test.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/demo/sftp/std/testing/log_demo_sftp_with_test.sh b/demo/sftp/std/testing/log_demo_sftp_with_test.sh index 546fe604..58768bc9 100755 --- a/demo/sftp/std/testing/log_demo_sftp_with_test.sh +++ b/demo/sftp/std/testing/log_demo_sftp_with_test.sh @@ -100,4 +100,7 @@ echo "If stuck use Ctrl+C to stop the script and cleanup." wait "$TEST_FILE_PID" echo "Finished executing ${TEST_FILE}" +echo "extracting TX/RX data from log file..." +./extract_txrx.sh $RUST_LOG_FILE + cleanup From d5ba86749b4cc1e7edb9c0e2a57ce45eefe590fa Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Wed, 17 Dec 2025 14:19:20 +1100 Subject: [PATCH 389/393] Config num_enum for no_std --- Cargo.lock | 36 ------------------------------------ sftp/Cargo.toml | 2 +- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0176d4c4..e9cad532 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1837,7 +1837,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.58", @@ -2140,15 +2139,6 @@ dependencies = [ "elliptic-curve", ] -[[package]] -name = "proc-macro-crate" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" -dependencies = [ - "toml_edit", -] - [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -3068,23 +3058,6 @@ dependencies = [ "syn 2.0.58", ] -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "toml_datetime", - "winnow", -] - [[package]] name = "typenum" version = "1.17.0" @@ -3496,15 +3469,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] - [[package]] name = "x25519-dalek" version = "2.0.1" diff --git a/sftp/Cargo.toml b/sftp/Cargo.toml index 704a9a84..f9af29d7 100644 --- a/sftp/Cargo.toml +++ b/sftp/Cargo.toml @@ -16,7 +16,7 @@ sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" } embedded-io-async = "0.6" -num_enum = {version = "0.7.4"} +num_enum = {version = "0.7.4", default-features = false} paste = "1.0" log = "0.4" embassy-sync = "0.7.2" From ce096d04705cbd335a3ff0ecb327b894659aebae Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 18 Dec 2025 14:42:56 +1100 Subject: [PATCH 390/393] Fixing format and orphan use --- sftp/src/bin/read_packets_from_file.rs | 84 +++++++++++-------- sftp/src/lib.rs | 4 +- sftp/src/proto.rs | 8 +- .../sftphandler/sftpoutputchannelhandler.rs | 6 +- sftp/src/sftpserver.rs | 23 +++-- src/error.rs | 2 - 6 files changed, 65 insertions(+), 62 deletions(-) diff --git a/sftp/src/bin/read_packets_from_file.rs b/sftp/src/bin/read_packets_from_file.rs index 37f9fb60..dcd07012 100644 --- a/sftp/src/bin/read_packets_from_file.rs +++ b/sftp/src/bin/read_packets_from_file.rs @@ -1,41 +1,42 @@ +use std::env; use std::fs::File; use std::io::{BufRead, BufReader}; -use std::env; +use sunset::sshwire::{SSHDecode, SSHSource}; use sunset_sftp::SftpSource; use sunset_sftp::protocol::{NameEntry, SftpPacket}; -use sunset::sshwire::{SSHDecode, SSHSource}; /// This program reads packets from a specified file and prints their byte representation. -/// -/// We assume that each line contains an u8. a way to get this format is to use the +/// +/// We assume that each line contains an u8. a way to get this format is to use the /// `demo/sftp/std/testing/extract_txrx.sh` on a sunset-demo-sftp-std log file. -/// +/// /// # Usage /// ``` -/// cargo run --package sunset-sftp --bin read_packets_from_file +/// cargo run --package sunset-sftp --bin read_packets_from_file /// ``` /// where `` is the path to the file containing the packets. -/// +/// fn main() { - let args: Vec = env::args().collect(); - + if args.len() < 2 { eprintln!("Usage: {} ", args[0]); std::process::exit(1); } - + let file_path = &args[1]; let file = File::open(file_path).expect("Failed to open file"); let reader = BufReader::new(file); let mut bytes: Vec = Vec::new(); - for (i,line) in reader.lines().enumerate() { + for (i, line) in reader.lines().enumerate() { match line { Ok(content) => { - let num = content.parse::().expect(format!("Failed to parse line {i} as u8").as_str()); + let num = content + .parse::() + .expect(format!("Failed to parse line {i} as u8").as_str()); bytes.push(num); } - Err(e) => eprintln!("Error reading line {}: {}",i, e), + Err(e) => eprintln!("Error reading line {}: {}", i, e), } } println!("Read {} u8 elements from file {}", bytes.len(), file_path); @@ -43,31 +44,36 @@ fn main() { let mut source = SftpSource::new(&bytes.as_slice()); while source.remaining() > 0 { match SftpPacket::decode(&mut source) { - Ok(packet) => { - - match packet { - SftpPacket::Name(req_id, name) => { - println!("SFTP Name: {:?} {:?}", req_id, name); - for i in 0..name.count { - println!("--({i}) Entry: {:?}", NameEntry::dec(&mut source).expect("Failed to decode NameEntry")); - } - } - SftpPacket::Handle(req_id, handle ) => { - println!("SFTP Handle: {:?} {:?}", req_id, handle); - } - SftpPacket::Attrs(req_id, attrs ) => { - println!("SFTP Attrs: {:?} {:?}", req_id, attrs); - } - SftpPacket::Data(req_id, data ) => { - println!("SFTP Data: {:?} {:?} bytes", req_id, data); - } - _ => { - println!("Decoded packet: {:?}", packet); + Ok(packet) => match packet { + SftpPacket::Name(req_id, name) => { + println!("SFTP Name: {:?} {:?}", req_id, name); + for i in 0..name.count { + println!( + "--({i}) Entry: {:?}", + NameEntry::dec(&mut source) + .expect("Failed to decode NameEntry") + ); } } - } + SftpPacket::Handle(req_id, handle) => { + println!("SFTP Handle: {:?} {:?}", req_id, handle); + } + SftpPacket::Attrs(req_id, attrs) => { + println!("SFTP Attrs: {:?} {:?}", req_id, attrs); + } + SftpPacket::Data(req_id, data) => { + println!("SFTP Data: {:?} {:?} bytes", req_id, data); + } + _ => { + println!("Decoded packet: {:?}", packet); + } + }, Err(e) => { - println!("Error decoding packet: {:?}. Up to: {:?}", e, source.buffer_used().len()); + println!( + "Error decoding packet: {:?}. Up to: {:?}", + e, + source.buffer_used().len() + ); break; } @@ -75,6 +81,10 @@ fn main() { let prev_used = used; used = source.buffer_used().len(); let last_used = used - prev_used; - println!("Last 9 bytes : {:?}, Lines {:?}-{used}, Counters: ({last_used}/{used}) [last/total decoded]\n", &source.buffer_used()[used-9..], prev_used+1 ); + println!( + "Last 9 bytes : {:?}, Lines {:?}-{used}, Counters: ({last_used}/{used}) [last/total decoded]\n", + &source.buffer_used()[used - 9..], + prev_used + 1 + ); } -} \ No newline at end of file +} diff --git a/sftp/src/lib.rs b/sftp/src/lib.rs index 885de9c0..46c2670c 100644 --- a/sftp/src/lib.rs +++ b/sftp/src/lib.rs @@ -66,7 +66,7 @@ mod sftpsource; pub use sftphandler::SftpHandler; /// Source of SFTP packets -/// +/// /// Used to decode SFTP packets from a byte slice pub use sftpsource::SftpSource; @@ -104,7 +104,6 @@ pub mod handles { /// SFTP Protocol types and structures pub mod protocol { - pub use crate::proto::SftpPacket; pub use crate::proto::Attrs; pub use crate::proto::FileHandle; pub use crate::proto::Filename; @@ -112,6 +111,7 @@ pub mod protocol { pub use crate::proto::NameEntry; pub use crate::proto::PFlags; pub use crate::proto::PathInfo; + pub use crate::proto::SftpPacket; pub use crate::proto::StatusCode; /// Constants that might be useful for SFTP developers pub mod constants { diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 9a317595..28f69695 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -992,12 +992,8 @@ mod proto_tests { fn test_data_roundtrip() { let data_slice = b"Hello, world!".as_slice(); let mut buff = [0u8; 512]; - let data_packet = SftpPacket::Data( - ReqId(10), - Data { - data: BinString(data_slice), - }, - ); + let data_packet = + SftpPacket::Data(ReqId(10), Data { data: BinString(data_slice) }); let mut sink = SftpSink::new(&mut buff); data_packet.encode_response(&mut sink).expect("Failed to encode response"); diff --git a/sftp/src/sftphandler/sftpoutputchannelhandler.rs b/sftp/src/sftphandler/sftpoutputchannelhandler.rs index e298990a..5f6b9f63 100644 --- a/sftp/src/sftphandler/sftpoutputchannelhandler.rs +++ b/sftp/src/sftphandler/sftpoutputchannelhandler.rs @@ -119,8 +119,10 @@ impl<'a, const N: usize> SftpOutputConsumer<'a, N> { ); let wl = self.ssh_chan_out.write(scanning_buffer).await?; debug!("Output Consumer: Written {:?} bytes ", wl); - if wl< scanning_buffer.len() { - debug!("Output Consumer: ChanOut accepted only part of the buffer"); + if wl < scanning_buffer.len() { + debug!( + "Output Consumer: ChanOut accepted only part of the buffer" + ); } trace!( "Output Consumer: Bytes written {:?}", diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index 2d511f27..b48836d9 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -1,6 +1,6 @@ use crate::error::{SftpError, SftpResult}; use crate::proto::{ - ENCODED_SSH_FXP_DATA_MIN_LENGTH, ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, + ENCODED_BASE_NAME_SFTP_PACKET_LENGTH, ENCODED_SSH_FXP_DATA_MIN_LENGTH, MAX_NAME_ENTRY_SIZE, NameEntry, PFlags, SftpNum, }; use crate::server::SftpSink; @@ -67,7 +67,7 @@ where Err(StatusCode::SSH_FX_OP_UNSUPPORTED) } /// Reads from a file that has previously being opened for reading - /// + /// /// ## Notes to the implementer: /// /// The implementer is expected to use the parameter `reply` [`DirReply`] to: @@ -77,11 +77,11 @@ where /// 1. Call `reply.send_header()` with the length of data to be sent /// 2. Call `reply.send_data()` once or multiple times to send all the data announced /// 3. Do not call `reply.send_eof()` during this [`readdir`] method call - /// + /// - /// If the length communicated in the header does not match the total length of the data + /// If the length communicated in the header does not match the total length of the data /// sent using `reply.send_data()`, the SFTP session will be broken. - /// + /// #[allow(unused)] async fn read( &mut self, @@ -137,7 +137,7 @@ where /// If the length communicated in the header does not match the total length of all /// the items sent using `reply.send_item()`, the SFTP session will be /// broken. - /// + /// /// The server is expected to keep track of the number of items that remain to be sent /// to the client since the client will only stop asking for more elements in the /// directory when a read dir request is answer with an reply.send_eof() @@ -208,7 +208,7 @@ impl<'g, const N: usize> ReadReply<'g, N> { req_id: ReqId, chan_out: &'g SftpOutputProducer<'g, N>, ) -> Self { - ReadReply { req_id, chan_out, data_len:0, data_sent_len:0 } + ReadReply { req_id, chan_out, data_len: 0, data_sent_len: 0 } } // TODO Make this enforceable @@ -257,7 +257,7 @@ impl<'g, const N: usize> ReadReply<'g, N> { } /// Indicates whether all the data announced in the header has been sent - /// + /// /// returns 0 when all data has been sent /// returns >0 when there is still data to be sent /// returns <0 when too much data has been sent @@ -270,7 +270,6 @@ impl<'g, const N: usize> ReadReply<'g, N> { req_id: ReqId, data_len: u32, ) -> Result<&'g [u8], SftpError> { - // length field (data_len + ENCODED_SSH_FXP_DATA_MIN_LENGTH).enc(sink)?; // packet type (1) @@ -309,7 +308,6 @@ mod read_reply_tests { u32::from_be_bytes(payload[..4].try_into().unwrap()) ); } - } /// Uses for [`DirReply`] to: @@ -346,7 +344,7 @@ impl<'g, const N: usize> DirReply<'g, N> { chan_out: &'g SftpOutputProducer<'g, N>, ) -> Self { // DirReply { chan_out: chan_out_wrapper, req_id } - DirReply { req_id, chan_out, data_len:0, data_sent_len:0 } + DirReply { req_id, chan_out, data_len: 0, data_sent_len: 0 } } // TODO Make this enforceable @@ -404,7 +402,7 @@ impl<'g, const N: usize> DirReply<'g, N> { } /// Indicates whether all the data announced in the header has been sent - /// + /// /// returns 0 when all data has been sent /// returns >0 when there is still data to be sent /// returns <0 when too much data has been sent @@ -428,7 +426,6 @@ impl<'g, const N: usize> DirReply<'g, N> { Ok(sink.payload_slice()) } - } #[cfg(test)] diff --git a/src/error.rs b/src/error.rs index 0ac33552..42333e8b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,8 +6,6 @@ use core::fmt::Arguments; use snafu::prelude::*; -use heapless::String; - use crate::channel::ChanNum; #[allow(unused_imports)] From 9a68d083a7c93c075b8f357ae0a19b2fe209553d Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 18 Dec 2025 14:54:36 +1100 Subject: [PATCH 391/393] Fixing docs format and docs for macros --- sftp/src/proto.rs | 19 ++++++++----------- sftp/src/sftpsource.rs | 4 +++- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/sftp/src/proto.rs b/sftp/src/proto.rs index 28f69695..962ade5f 100644 --- a/sftp/src/proto.rs +++ b/sftp/src/proto.rs @@ -360,12 +360,6 @@ impl SSHEncode for Name { } } -// TODO: Is this really necessary? -#[derive(Debug, SSHEncode, SSHDecode)] -pub struct ResponseAttributes { - pub attrs: Attrs, -} - // Requests/Responses data types #[derive(Debug, SSHEncode, SSHDecode, Clone, Copy, PartialEq, Eq)] @@ -437,11 +431,11 @@ impl SSHEncode for StatusCode { // TODO: Implement extensions. Low in priority /// Provided to provide a mechanism to implement extensions -#[derive(Debug, SSHEncode, SSHDecode)] -pub struct ExtPair<'a> { - pub name: &'a str, - pub data: BinString<'a>, -} +// #[derive(Debug, SSHEncode, SSHDecode)] +// pub struct ExtPair<'a> { +// pub name: &'a str, +// pub data: BinString<'a>, +// } /// Files attributes to describe Files as SFTP v3 specification /// @@ -680,12 +674,15 @@ macro_rules! sftpmessages { #[derive(Debug)] pub enum SftpPacket<'a> { $( + #[doc = concat!("Initialization packet: ", $init_ssh_fxp_name)] $init_packet_variant($init_packet_type), )* $( + #[doc = concat!("Request packet: ", $request_ssh_fxp_name)] $request_packet_variant(ReqId, $request_packet_type), )* $( + #[doc = concat!("Response packet: ", $response_ssh_fxp_name)] $response_packet_variant(ReqId, $response_packet_type), )* diff --git a/sftp/src/sftpsource.rs b/sftp/src/sftpsource.rs index 774622d5..6818c2d4 100644 --- a/sftp/src/sftpsource.rs +++ b/sftp/src/sftpsource.rs @@ -136,7 +136,9 @@ impl<'de> SftpSource<'de> { Ok(u32::from_be_bytes(bytes)) } } - + /// Returns a slice on the used portion of the held buffer. + /// + /// This does not modify the internal index pub fn buffer_used(&self) -> &[u8] { &self.buffer[..self.index] } From 3b3b38b1c9f0497310263ef062313321bb37bc59 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Thu, 18 Dec 2025 15:21:04 +1100 Subject: [PATCH 392/393] Adding sftp to the CI fixing and passing it --- demo/sftp/std/src/main.rs | 26 +++++--------------------- sftp/src/sftpserver.rs | 36 ++++++++++++++++++++---------------- testing/ci.sh | 9 +++++++++ 3 files changed, 34 insertions(+), 37 deletions(-) diff --git a/demo/sftp/std/src/main.rs b/demo/sftp/std/src/main.rs index 3486c9d8..526f7ebf 100644 --- a/demo/sftp/std/src/main.rs +++ b/demo/sftp/std/src/main.rs @@ -219,27 +219,11 @@ async fn listen( #[embassy_executor::main] async fn main(spawner: Spawner) { - let log_path = std::env::var("RUST_LOG_FILE").ok(); - - let mut builder = env_logger::Builder::new(); - - builder.filter_level(log::LevelFilter::Trace); - // if std::env::var("RUST_LOG").is_err() { - // } else { - // builder.filter_level(log::LevelFilter::Debug); - // } - builder.format_timestamp_nanos(); - - // if let Some(path) = log_path { - // let file = std::fs::File::create(path).expect("Failed to create log file"); - // builder.target(env_logger::Target::Pipe(Box::new(file))); - // } else { - // builder.target(env_logger::Target::Stdout); - // } - - builder.target(env_logger::Target::Stdout); - - builder.init(); + env_logger::Builder::new() + .filter_level(log::LevelFilter::Trace) + .format_timestamp_nanos() + .target(env_logger::Target::Stdout) + .init(); spawner.spawn(main_task(spawner)).unwrap(); } diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index b48836d9..b8cef280 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -83,20 +83,22 @@ where /// sent using `reply.send_data()`, the SFTP session will be broken. /// #[allow(unused)] - async fn read( + fn read( &mut self, opaque_file_handle: &T, offset: u64, len: u32, reply: &mut ReadReply<'_, N>, - ) -> SftpResult<()> { - log::error!( - "SftpServer Read operation not defined: handle = {:?}, offset = {:?}, len = {:?}", - opaque_file_handle, - offset, - len - ); - Err(SftpError::FileServerError(StatusCode::SSH_FX_OP_UNSUPPORTED)) + ) -> impl core::future::Future> { + async move { + log::error!( + "SftpServer Read operation not defined: handle = {:?}, offset = {:?}, len = {:?}", + opaque_file_handle, + offset, + len + ); + Err(SftpError::FileServerError(StatusCode::SSH_FX_OP_UNSUPPORTED)) + } } /// Writes to a file that has previously being opened for writing fn write( @@ -143,16 +145,18 @@ where /// directory when a read dir request is answer with an reply.send_eof() /// #[allow(unused_variables)] - async fn readdir( + fn readdir( &mut self, opaque_dir_handle: &T, reply: &mut DirReply<'_, N>, - ) -> SftpOpResult<()> { - log::error!( - "SftpServer ReadDir operation not defined: handle = {:?}", - opaque_dir_handle - ); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + ) -> impl core::future::Future> { + async move { + log::error!( + "SftpServer ReadDir operation not defined: handle = {:?}", + opaque_dir_handle + ); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } } /// Provides the real path of the directory specified diff --git a/testing/ci.sh b/testing/ci.sh index 22b540a1..49b56766 100755 --- a/testing/ci.sh +++ b/testing/ci.sh @@ -74,6 +74,15 @@ cargo build --release --no-default-features --features w5500,romfw ) size target/thumbv6m-none-eabi/release/sunset-demo-picow | tee "$OUT/picow-size.txt" +( +cd demo/sftp/std +cargo build --release +cargo test --release +cargo bloat --release -n 100 | tee "$OUT/sftp-std-bloat.txt" +cargo bloat --release --crates | tee "$OUT/sftp-std-bloat-crates.txt" +) +size ./target/release/sunset-demo-sftp-std | tee "$OUT/sftp-std-size.txt" + ( cd fuzz cargo check --features nofuzz --profile fuzz From 9d76de8a261736e8b4b8bfc35b61d6feccbf63a8 Mon Sep 17 00:00:00 2001 From: jubeormk1 Date: Fri, 16 Jan 2026 13:18:29 +1100 Subject: [PATCH 393/393] Changing all SftpServer signatures to async (or impl Future with async move block) I have done this in the view that all that operations might need to run async code. Demo changed accordingly and test passing --- demo/sftp/std/src/demosftpserver.rs | 16 +++-- sftp/src/sftphandler/sftphandler.rs | 26 ++++++-- sftp/src/sftpserver.rs | 100 +++++++++++++++++++--------- 3 files changed, 96 insertions(+), 46 deletions(-) diff --git a/demo/sftp/std/src/demosftpserver.rs b/demo/sftp/std/src/demosftpserver.rs index 57bbaada..9d7fd826 100644 --- a/demo/sftp/std/src/demosftpserver.rs +++ b/demo/sftp/std/src/demosftpserver.rs @@ -107,7 +107,7 @@ impl DemoSftpServer { } impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { - fn open( + async fn open( &mut self, filename: &str, mode: &PFlags, @@ -153,7 +153,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { fh } - fn opendir(&mut self, dir: &str) -> SftpOpResult { + async fn opendir(&mut self, dir: &str) -> SftpOpResult { debug!("Open Directory = {:?}", dir); let dir_handle = self.handles_manager.insert( @@ -172,7 +172,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { dir_handle } - fn realpath(&mut self, dir: &str) -> SftpOpResult> { + async fn realpath(&mut self, dir: &str) -> SftpOpResult> { info!("finding path for: {:?}", dir); let name_entry = NameEntry { filename: Filename::from(self.base_path.as_str()), @@ -191,7 +191,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { Ok(name_entry) } - fn close( + async fn close( &mut self, opaque_file_handle: &DemoOpaqueFileHandle, ) -> SftpOpResult<()> { @@ -320,7 +320,7 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { Err(StatusCode::SSH_FX_PERMISSION_DENIED.into()) } - fn write( + async fn write( &mut self, opaque_file_handle: &DemoOpaqueFileHandle, offset: u64, @@ -416,7 +416,11 @@ impl SftpServer<'_, DemoOpaqueFileHandle> for DemoSftpServer { } } - fn stats(&mut self, follow_links: bool, file_path: &str) -> SftpOpResult { + async fn stats( + &mut self, + follow_links: bool, + file_path: &str, + ) -> SftpOpResult { log::debug!("SftpServer ListStats: file_path = {:?}", file_path); let file_path = Path::new(file_path); diff --git a/sftp/src/sftphandler/sftphandler.rs b/sftp/src/sftphandler/sftphandler.rs index 15baab8b..d4f3e85f 100644 --- a/sftp/src/sftphandler/sftphandler.rs +++ b/sftp/src/sftphandler/sftphandler.rs @@ -159,11 +159,11 @@ where let data = &buf[..used]; buf = &buf[used..]; - match self.file_server.write( - &T::try_from(&write.handle)?, - *offset, - data, - ) { + match self + .file_server + .write(&T::try_from(&write.handle)?, *offset, data) + .await + { Ok(_) => { if remaining_data == 0 { output_producer @@ -427,7 +427,11 @@ where self.state = HandlerState::Idle; } SftpPacket::LStat(req_id, LStat { file_path: path }) => { - match self.file_server.stats(false, path.as_str()?) { + match self + .file_server + .stats(false, path.as_str()?) + .await + { Ok(attrs) => { debug!( "List stats for {} is {:?}", @@ -457,7 +461,11 @@ where self.state = HandlerState::Idle; } SftpPacket::Stat(req_id, Stat { file_path: path }) => { - match self.file_server.stats(true, path.as_str()?) { + match self + .file_server + .stats(true, path.as_str()?) + .await + { Ok(attrs) => { debug!( "List stats for {} is {:?}", @@ -530,6 +538,7 @@ where match self .file_server .opendir(open_dir.dirname.as_str()?) + .await { Ok(opaque_file_handle) => { let response = SftpPacket::Handle( @@ -560,6 +569,7 @@ where match self .file_server .close(&T::try_from(&close.handle)?) + .await { Ok(_) => { output_producer @@ -594,6 +604,7 @@ where match self .file_server .open(open.filename.as_str()?, &open.pflags) + .await { Ok(opaque_file_handle) => { let response = SftpPacket::Handle( @@ -624,6 +635,7 @@ where match self .file_server .realpath(path_info.path.as_str()?) + .await { Ok(name_entry) => { let mut dir_reply = diff --git a/sftp/src/sftpserver.rs b/sftp/src/sftpserver.rs index b8cef280..c43201f2 100644 --- a/sftp/src/sftpserver.rs +++ b/sftp/src/sftpserver.rs @@ -51,20 +51,34 @@ where T: OpaqueFileHandle, { /// Opens a file for reading/writing - fn open(&'_ mut self, path: &str, mode: &PFlags) -> SftpOpResult { - log::error!( - "SftpServer Open operation not defined: path = {:?}, attrs = {:?}", - path, - mode - ); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + fn open( + &'_ mut self, + path: &str, + mode: &PFlags, + ) -> impl core::future::Future> { + async move { + log::error!( + "SftpServer Open operation not defined: path = {:?}, attrs = {:?}", + path, + mode + ); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } } /// Close either a file or directory handle - fn close(&mut self, handle: &T) -> SftpOpResult<()> { - log::error!("SftpServer Close operation not defined: handle = {:?}", handle); + fn close( + &mut self, + handle: &T, + ) -> impl core::future::Future> { + async move { + log::error!( + "SftpServer Close operation not defined: handle = {:?}", + handle + ); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } } /// Reads from a file that has previously being opened for reading /// @@ -106,20 +120,27 @@ where opaque_file_handle: &T, offset: u64, buf: &[u8], - ) -> SftpOpResult<()> { - log::error!( - "SftpServer Write operation not defined: handle = {:?}, offset = {:?}, buf = {:?}", - opaque_file_handle, - offset, - buf - ); - Ok(()) + ) -> impl core::future::Future> { + async move { + log::error!( + "SftpServer Write operation not defined: handle = {:?}, offset = {:?}, buf = {:?}", + opaque_file_handle, + offset, + buf + ); + Ok(()) + } } /// Opens a directory and returns a handle - fn opendir(&mut self, dir: &str) -> SftpOpResult { - log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + fn opendir( + &mut self, + dir: &str, + ) -> impl core::future::Future> { + async move { + log::error!("SftpServer OpenDir operation not defined: dir = {:?}", dir); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } } /// Reads the list of items in a directory and returns them using the [`DirReply`] @@ -160,23 +181,36 @@ where } /// Provides the real path of the directory specified - fn realpath(&mut self, dir: &str) -> SftpOpResult> { - log::error!("SftpServer RealPath operation not defined: dir = {:?}", dir); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + fn realpath( + &mut self, + dir: &str, + ) -> impl core::future::Future>> { + async move { + log::error!( + "SftpServer RealPath operation not defined: dir = {:?}", + dir + ); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } } /// Provides the stats of the given file path - fn stats(&mut self, follow_links: bool, file_path: &str) -> SftpOpResult { - log::error!( - "SftpServer Stats operation not defined: follow_link = {:?}, \ - file_path = {:?}", - follow_links, - file_path - ); - Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + fn stats( + &mut self, + follow_links: bool, + file_path: &str, + ) -> impl core::future::Future> { + async move { + log::error!( + "SftpServer Stats operation not defined: follow_link = {:?}, \ + file_path = {:?}", + follow_links, + file_path + ); + Err(StatusCode::SSH_FX_OP_UNSUPPORTED) + } } } - // TODO Define this /// A reference structure passed to the [`SftpServer::read()`] method to /// allow replying with the read data.