diff --git a/Cargo.lock b/Cargo.lock index c711862..9301c3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -535,6 +544,7 @@ name = "flux-sftp" version = "0.1.0" dependencies = [ "chrono", + "regex", "russh", "russh-sftp", "tokio", @@ -1222,6 +1232,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rfc6979" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index ab36e39..4f47fdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] chrono = "0.4.41" +regex = "1.11.1" russh = "0.52.1" russh-sftp = "2.1.1" tokio = { version = "1.45.1", features = ["full"] } diff --git a/src/main.rs b/src/main.rs index 68074ca..7f68005 100644 --- a/src/main.rs +++ b/src/main.rs @@ -67,7 +67,7 @@ impl SshHandler for SshSession { ) -> Result<(), Self::Error> { if name == "sftp" { session.channel_success(channel_id)?; - let jail_dir = format!("/srv/sftp/{}", self.user.take().unwrap()); + let jail_dir = format!("/srv/sftp/{}", self.user.as_ref().unwrap()); let sftp_handler = SftpSession::new(jail_dir); russh_sftp::server::run(self.channel.take().ok_or(Self::Error::WrongChannel)?.into_stream(), sftp_handler).await; } diff --git a/src/sftp.rs b/src/sftp.rs index 10a6327..da95d72 100644 --- a/src/sftp.rs +++ b/src/sftp.rs @@ -1,14 +1,20 @@ -use std::{collections::HashMap, os::unix::fs::MetadataExt}; +use std::{collections::HashMap, io::SeekFrom, os::unix::fs::MetadataExt}; use chrono::{Local, TimeZone}; -use russh_sftp::{protocol::{File, FileAttributes, Handle, Name, Status, StatusCode}, server::Handler as SftpHandler}; +use regex::Regex; +use russh_sftp::{protocol::{Attrs, Data, File, FileAttributes, Handle as SftpHandle, Name, OpenFlags, Status, StatusCode}, server::Handler as SftpHandler}; -use tokio::fs::{self, ReadDir}; +use tokio::{fs::{self, OpenOptions, ReadDir}, io::{AsyncReadExt, AsyncSeekExt}}; + +enum Handle { + Dir(ReadDir), + File(fs::File) +} pub struct SftpSession { jail_dir: String, cwd: String, - handles: HashMap + handles: HashMap } impl SftpSession { @@ -29,34 +35,107 @@ impl SftpHandler for SftpSession { id: u32, path: String, ) -> Result { - let path_parts = path.split('/'); - for path_part in path_parts { - match path_part { - ".." => { - if self.cwd != "/" { - if let Some(pos) = self.cwd.rfind('/') { - self.cwd.truncate(pos); - } - } - }, - "." => {}, - _ => self.cwd.push_str(&format!("/{}", path_part)) - } + println!("realpath called, path: {}", path); + + let re_1 = Regex::new(r"/[^/]+/\.\.").unwrap(); + self.cwd = re_1.replace_all(&path, "").to_string(); + while re_1.is_match(&self.cwd) { + self.cwd = re_1.replace_all(&self.cwd, "").to_string(); + } + + let re_2 = Regex::new(r"/\.").unwrap(); + self.cwd = re_2.replace_all(&self.cwd, "").to_string(); + + if self.cwd == "." || self.cwd == "" { + self.cwd = String::from("/"); } Ok(Name { id, files: vec![File::dummy(&self.cwd)] }) } + async fn open( + &mut self, + id: u32, + filename: String, + pflags: OpenFlags, + _attrs: FileAttributes, + ) -> Result { + println!("open called, path: {}", filename); + let filename = format!("{}/{}", self.jail_dir, filename); + let mut options = OpenOptions::new(); + if pflags.contains(OpenFlags::READ){ + options.read(true); + } + if pflags.contains(OpenFlags::WRITE){ + options.write(true); + } + if pflags.contains(OpenFlags::APPEND){ + options.append(true); + } + if pflags.contains(OpenFlags::CREATE){ + options.create(true); + } + if pflags.contains(OpenFlags::TRUNCATE){ + options.truncate(true); + } + match options.open(&filename).await { + Ok(file) => { + self.handles.insert(filename.clone(), Handle::File(file)); + Ok(SftpHandle { id, handle: filename }) + } + Err(_) => Err(StatusCode::NoSuchFile) + } + } + + async fn read( + &mut self, + id: u32, + handle: String, + offset: u64, + len: u32, + ) -> Result { + if let Handle::File(file) = self.handles.get_mut(&handle).unwrap() { + let mut buf = vec![0u8; len as usize]; + match file.seek(SeekFrom::Start(offset)).await { + Ok(_) => { + match file.read(&mut buf).await { + Ok(bytes) => { + if bytes != 0 { + buf.truncate(bytes); + Ok(Data { id, data: buf }) + } + else { + Err(StatusCode::Eof) + } + } + Err(e) => { + println!("Error in reading from offset in file: {}", e); + Err(StatusCode::Failure) + } + } + } + Err(e) => { + println!("Error in seeking offset in file: {}", e); + Err(StatusCode::Failure) + } + } + } + else { + Err(StatusCode::Ok) + } + } + async fn opendir( &mut self, id: u32, path: String, - ) -> Result { + ) -> Result { + println!("opendir called"); let path = format!("{}/{}", self.jail_dir, path); match fs::read_dir(&path).await { Ok(entries) => { - self.handles.insert(path.clone(), entries); - Ok(Handle { id, handle: path }) + self.handles.insert(path.clone(), Handle::Dir(entries)); + Ok(SftpHandle { id, handle: path }) } Err(e) => { println!("Error in reading dir: {}", e); @@ -70,30 +149,37 @@ impl SftpHandler for SftpSession { id: u32, handle: String, ) -> Result { - match self.handles.get_mut(&handle).unwrap().next_entry().await { - Ok(Some(entry)) => { - let metadata = entry.metadata().await.unwrap(); - let dt = Local.timestamp_opt(metadata.mtime(), 0).unwrap(); - let longname = format!("{} {} {}", metadata.size(), dt.format("%b %e %Y"), entry.file_name().to_string_lossy()); - Ok(Name { id, files: vec![ - File { - filename: entry.file_name().to_string_lossy().into(), - longname: longname, - attrs: FileAttributes { - size: Some(metadata.size()), - atime: Some(metadata.atime() as u32), - mtime: Some(metadata.mtime() as u32), - ..Default::default() + println!("readdir called"); + if let Handle::Dir(handle) = self.handles.get_mut(&handle).unwrap() { + match handle.next_entry().await { + Ok(Some(entry)) => { + let metadata = entry.metadata().await.unwrap(); + let dt = Local.timestamp_opt(metadata.mtime(), 0).unwrap(); + let longname = format!("{} {} {}", metadata.size(), dt.format("%b %e %Y"), entry.file_name().to_string_lossy()); + Ok(Name { id, files: vec![ + File { + filename: entry.file_name().to_string_lossy().into(), + longname: longname, + attrs: FileAttributes { + size: Some(metadata.size()), + atime: Some(metadata.atime() as u32), + mtime: Some(metadata.mtime() as u32), + ..Default::default() + } } - } - ] }) - } - Ok(None) => Err(StatusCode::Eof), - Err(e) => { - println!("Error listing file: {}", e); - Err(StatusCode::Failure) + ] }) + } + Ok(None) => Err(StatusCode::Eof), + Err(e) => { + println!("Error listing file: {}", e); + Err(StatusCode::Failure) + } } } + else { + println!("handle is not a dirhandle"); + Err(StatusCode::Failure) + } } async fn close( @@ -101,6 +187,7 @@ impl SftpHandler for SftpSession { id: u32, handle: String, ) -> Result { + println!("close called"); self.handles.remove(&handle); Ok(Status { id, @@ -110,5 +197,60 @@ impl SftpHandler for SftpSession { }) } + async fn stat( + &mut self, + id: u32, + path: String, + ) -> Result { + println!("stat called"); + let path = format!("{}/{}", self.jail_dir, path); + match fs::metadata(path).await { + Ok(metadata) => Ok(Attrs { id, attrs: FileAttributes { + size: Some(metadata.size()), + uid: Some(metadata.uid()), + user: None, + gid: Some(metadata.gid()), + group: None, + permissions: Some(metadata.mode()), + atime: Some(metadata.atime() as u32), + mtime: Some(metadata.mtime() as u32), + ..Default::default() + }}), + Err(_) => Err(StatusCode::NoSuchFile) + } + } + + async fn lstat( + &mut self, + id: u32, + path: String, + ) -> Result { + println!("lstat called"); + let path = format!("{}/{}", self.jail_dir, path); + match fs::symlink_metadata(path).await { + Ok(metadata) => Ok(Attrs { id, attrs: FileAttributes { + size: Some(metadata.size()), + uid: Some(metadata.uid()), + user: None, + gid: Some(metadata.gid()), + group: None, + permissions: Some(metadata.mode()), + atime: Some(metadata.atime() as u32), + mtime: Some(metadata.mtime() as u32) + }}), + Err(_) => Err(StatusCode::OpUnsupported) + } + + } + + async fn fstat( + &mut self, + id: u32, + handle: String, + ) -> Result { + println!("fstat called"); + self.stat(id, handle).await + } + }