// SFTP协议实现(Phase 7) // 参考OpenSSH sftp-server.c和draft-ietf-secsh-filexfer-02.txt use crate::vfs::open_flags::OpenFlags; use crate::vfs::{VfsBackend, VfsDirEntry, VfsFile}; use anyhow::{anyhow, Context, Result}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use log::{debug, info, warn}; use std::fs; use std::io::{SeekFrom, Write}; use std::os::unix::fs::MetadataExt; use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; /// SFTP packet类型(参考draft-ietf-secsh-filexfer-02.txt) #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum SftpPacketType { SSH_FXP_INIT = 1, SSH_FXP_VERSION = 2, SSH_FXP_OPEN = 3, SSH_FXP_CLOSE = 4, SSH_FXP_READ = 5, SSH_FXP_WRITE = 6, SSH_FXP_LSTAT = 7, SSH_FXP_FSTAT = 8, SSH_FXP_SETSTAT = 9, SSH_FXP_FSETSTAT = 10, SSH_FXP_OPENDIR = 11, SSH_FXP_READDIR = 12, SSH_FXP_REMOVE = 13, SSH_FXP_MKDIR = 14, SSH_FXP_RMDIR = 15, SSH_FXP_REALPATH = 16, SSH_FXP_STAT = 17, SSH_FXP_RENAME = 18, SSH_FXP_READLINK = 19, SSH_FXP_SYMLINK = 20, SSH_FXP_STATUS = 101, SSH_FXP_HANDLE = 102, SSH_FXP_DATA = 103, SSH_FXP_NAME = 104, SSH_FXP_ATTRS = 105, SSH_FXP_EXTENDED = 200, SSH_FXP_EXTENDED_REPLY = 201, } impl TryFrom for SftpPacketType { type Error = anyhow::Error; fn try_from(value: u8) -> Result { match value { 1 => Ok(SftpPacketType::SSH_FXP_INIT), 2 => Ok(SftpPacketType::SSH_FXP_VERSION), 3 => Ok(SftpPacketType::SSH_FXP_OPEN), 4 => Ok(SftpPacketType::SSH_FXP_CLOSE), 5 => Ok(SftpPacketType::SSH_FXP_READ), 6 => Ok(SftpPacketType::SSH_FXP_WRITE), 7 => Ok(SftpPacketType::SSH_FXP_LSTAT), 8 => Ok(SftpPacketType::SSH_FXP_FSTAT), 9 => Ok(SftpPacketType::SSH_FXP_SETSTAT), 10 => Ok(SftpPacketType::SSH_FXP_FSETSTAT), 11 => Ok(SftpPacketType::SSH_FXP_OPENDIR), 12 => Ok(SftpPacketType::SSH_FXP_READDIR), 13 => Ok(SftpPacketType::SSH_FXP_REMOVE), 14 => Ok(SftpPacketType::SSH_FXP_MKDIR), 15 => Ok(SftpPacketType::SSH_FXP_RMDIR), 16 => Ok(SftpPacketType::SSH_FXP_REALPATH), 17 => Ok(SftpPacketType::SSH_FXP_STAT), 18 => Ok(SftpPacketType::SSH_FXP_RENAME), 19 => Ok(SftpPacketType::SSH_FXP_READLINK), 20 => Ok(SftpPacketType::SSH_FXP_SYMLINK), 101 => Ok(SftpPacketType::SSH_FXP_STATUS), 102 => Ok(SftpPacketType::SSH_FXP_HANDLE), 103 => Ok(SftpPacketType::SSH_FXP_DATA), 104 => Ok(SftpPacketType::SSH_FXP_NAME), 105 => Ok(SftpPacketType::SSH_FXP_ATTRS), 200 => Ok(SftpPacketType::SSH_FXP_EXTENDED), 201 => Ok(SftpPacketType::SSH_FXP_EXTENDED_REPLY), _ => Err(anyhow!("Unknown SFTP packet type: {}", value)), } } } /// SFTP状态码(参考draft-ietf-secsh-filexfer-02.txt) #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u32)] pub enum SftpStatus { 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, } /// SFTP文件标志(参考draft-ietf-secsh-filexfer-02.txt) pub struct SftpFileFlags; impl SftpFileFlags { pub const SSH_FXF_READ: u32 = 0x00000001; pub const SSH_FXF_WRITE: u32 = 0x00000002; pub const SSH_FXF_APPEND: u32 = 0x00000004; pub const SSH_FXF_CREAT: u32 = 0x00000008; pub const SSH_FXF_TRUNC: u32 = 0x00000010; pub const SSH_FXF_EXCL: u32 = 0x00000020; } /// SFTP文件属性标志(参考draft-ietf-secsh-filexfer-02.txt) pub struct SftpAttrFlags; impl SftpAttrFlags { pub const SSH_FILEXFER_ATTR_SIZE: u32 = 0x00000001; pub const SSH_FILEXFER_ATTR_UIDGID: u32 = 0x00000002; pub const SSH_FILEXFER_ATTR_PERMISSIONS: u32 = 0x00000004; pub const SSH_FILEXFER_ATTR_ACMODTIME: u32 = 0x00000008; pub const SSH_FILEXFER_ATTR_EXTENDED: u32 = 0x80000000; } /// SFTP文件属性(参考draft-ietf-secsh-filexfer-02.txt) #[derive(Debug, Clone)] pub struct SftpAttrs { pub flags: u32, pub size: Option, pub uid: Option, pub gid: Option, pub permissions: Option, pub atime: Option, pub mtime: Option, pub extended: Vec<(String, String)>, } impl Default for SftpAttrs { fn default() -> Self { Self::new() } } impl SftpAttrs { pub fn new() -> Self { Self { flags: 0, size: None, uid: None, gid: None, permissions: None, atime: None, mtime: None, extended: Vec::new(), } } pub fn from_metadata(metadata: &fs::Metadata) -> Self { let mut attrs = Self::new(); attrs.flags = SftpAttrFlags::SSH_FILEXFER_ATTR_SIZE | SftpAttrFlags::SSH_FILEXFER_ATTR_UIDGID | SftpAttrFlags::SSH_FILEXFER_ATTR_PERMISSIONS | SftpAttrFlags::SSH_FILEXFER_ATTR_ACMODTIME; attrs.size = Some(metadata.len()); attrs.permissions = Some(metadata.permissions().mode()); attrs.uid = Some(metadata.uid()); attrs.gid = Some(metadata.gid()); if let Ok(atime) = metadata.accessed() { attrs.atime = atime .duration_since(std::time::UNIX_EPOCH) .ok() .map(|d| d.as_secs() as u32); } if let Ok(mtime) = metadata.modified() { attrs.mtime = mtime .duration_since(std::time::UNIX_EPOCH) .ok() .map(|d| d.as_secs() as u32); } attrs } pub fn from_vfs_stat(stat: &crate::vfs::VfsStat) -> Self { let mut attrs = Self::new(); attrs.flags = SftpAttrFlags::SSH_FILEXFER_ATTR_SIZE | SftpAttrFlags::SSH_FILEXFER_ATTR_UIDGID | SftpAttrFlags::SSH_FILEXFER_ATTR_PERMISSIONS | SftpAttrFlags::SSH_FILEXFER_ATTR_ACMODTIME; attrs.size = Some(stat.size); attrs.permissions = Some(stat.mode); attrs.uid = Some(stat.uid); attrs.gid = Some(stat.gid); if let Ok(d) = stat.atime.duration_since(std::time::UNIX_EPOCH) { attrs.atime = Some(d.as_secs() as u32); } if let Ok(d) = stat.mtime.duration_since(std::time::UNIX_EPOCH) { attrs.mtime = Some(d.as_secs() as u32); } attrs } pub fn serialize(&self) -> Result> { debug!("Serializing SftpAttrs: flags=0x{:08x}, size={:?}, uid={:?}, gid={:?}, permissions=0x{:08x}, atime={:?}, mtime={:?}", self.flags, self.size, self.uid, self.gid, self.permissions.unwrap_or(0), self.atime, self.mtime, ); let mut buffer = Vec::new(); buffer .write_u32::(self.flags) .with_context(|| "serialize attrs flags")?; if self.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_SIZE != 0 { if let Some(size) = self.size { buffer .write_u64::(size) .with_context(|| "serialize attrs size")?; } } if self.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_UIDGID != 0 { if let (Some(uid), Some(gid)) = (self.uid, self.gid) { buffer .write_u32::(uid) .with_context(|| "serialize attrs uid")?; buffer .write_u32::(gid) .with_context(|| "serialize attrs gid")?; } } if self.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_PERMISSIONS != 0 { if let Some(permissions) = self.permissions { buffer .write_u32::(permissions) .with_context(|| "serialize attrs perms")?; } } if self.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_ACMODTIME != 0 { if let (Some(atime), Some(mtime)) = (self.atime, self.mtime) { buffer .write_u32::(atime) .with_context(|| "serialize attrs atime")?; buffer .write_u32::(mtime) .with_context(|| "serialize attrs mtime")?; } } if self.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_EXTENDED != 0 { buffer .write_u32::(self.extended.len() as u32) .with_context(|| "serialize attrs ext count")?; for (name, value) in &self.extended { buffer .write_u32::(name.len() as u32) .with_context(|| "serialize attrs ext name len")?; buffer .write_all(name.as_bytes()) .with_context(|| "serialize attrs ext name")?; buffer .write_u32::(value.len() as u32) .with_context(|| "serialize attrs ext value len")?; buffer .write_all(value.as_bytes()) .with_context(|| "serialize attrs ext value")?; } } Ok(buffer) } } /// SFTP handle(文件或目录句柄) pub struct SftpHandle { pub id: u32, pub path: PathBuf, pub handle_type: SftpHandleType, pub file: Option>, pub dir_entries: Option>, pub write_mode: bool, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum SftpHandleType { File, Directory, } /// SFTP处理管理器(参考OpenSSH sftp-server.c) pub struct SftpHandler { root_dir: PathBuf, vfs: Box, next_handle_id: u32, handles: std::collections::HashMap, maxpacket: u32, restrict_absolute: bool, upload_hook: Option>, user_uuid: String, } impl SftpHandler { /// 最大并发handle数(防止资源耗尽) const MAX_HANDLES: usize = 4096; /// 单次读写最大字节数(1MB,防止OOM) const MAX_XFER_SIZE: u32 = 1_048_576; /// 单次hash最大字节数(256MB,平衡安全与性能) const MAX_HASH_SIZE: u64 = 268_435_456; // ⭐⭐⭐⭐⭐ Phase 4: 修改 new() 方法,接受 maxpack 参数 pub fn new( root_dir: PathBuf, vfs: Box, maxpacket: u32, upload_hook: Option>, user_uuid: String, ) -> Self { let canonical_root = root_dir.canonicalize().unwrap_or(root_dir); Self { root_dir: canonical_root, vfs, next_handle_id: 0, handles: std::collections::HashMap::new(), maxpacket, restrict_absolute: false, upload_hook, user_uuid, } } /// 设置 restrict_absolute 模式(chroot-like) pub fn set_restrict_absolute(&mut self, restrict: bool) { self.restrict_absolute = restrict; } /// 处理SFTP请求(参考OpenSSH sftp-server.c: process()) pub fn handle_request(&mut self, data: &[u8]) -> Result> { if data.is_empty() { return Err(anyhow!("Empty SFTP request")); } let packet_type = SftpPacketType::try_from(data[0])?; info!("Processing SFTP request: {:?}", packet_type); match packet_type { SftpPacketType::SSH_FXP_INIT => self.handle_init(data), SftpPacketType::SSH_FXP_OPEN => self.handle_open(data), SftpPacketType::SSH_FXP_CLOSE => self.handle_close(data), SftpPacketType::SSH_FXP_READ => self.handle_read(data), SftpPacketType::SSH_FXP_WRITE => self.handle_write(data), SftpPacketType::SSH_FXP_LSTAT => self.handle_lstat(data), SftpPacketType::SSH_FXP_FSTAT => self.handle_fstat(data), SftpPacketType::SSH_FXP_SETSTAT => self.handle_setstat(data), SftpPacketType::SSH_FXP_FSETSTAT => self.handle_fsetstat(data), SftpPacketType::SSH_FXP_OPENDIR => self.handle_opendir(data), SftpPacketType::SSH_FXP_READDIR => self.handle_readdir(data), SftpPacketType::SSH_FXP_REMOVE => self.handle_remove(data), SftpPacketType::SSH_FXP_MKDIR => self.handle_mkdir(data), SftpPacketType::SSH_FXP_RMDIR => self.handle_rmdir(data), SftpPacketType::SSH_FXP_REALPATH => self.handle_realpath(data), SftpPacketType::SSH_FXP_STAT => self.handle_stat(data), SftpPacketType::SSH_FXP_RENAME => self.handle_rename(data), SftpPacketType::SSH_FXP_READLINK => self.handle_readlink(data), SftpPacketType::SSH_FXP_SYMLINK => self.handle_symlink(data), SftpPacketType::SSH_FXP_EXTENDED => self.handle_extended(data), _ => { warn!("Unsupported SFTP packet type: {:?}", packet_type); Err(anyhow!("Unsupported SFTP packet type")) } } } /// 处理SSH_FXP_INIT(参考OpenSSH sftp-server.c: process_init()) fn handle_init(&self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_INIT"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let version = cursor.read_u32::()?; info!("Client SFTP version: {}", version); let response = self.build_version_response(3)?; Ok(response) } /// 处理SSH_FXP_OPEN(参考OpenSSH sftp-server.c: process_open()) fn handle_open(&mut self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_OPEN"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let path = read_sftp_string(&mut cursor)?; let pflags = cursor.read_u32::()?; let _attrs = read_sftp_attrs(&mut cursor)?; info!( "SSH_FXP_OPEN: id={}, path={}, pflags={:#x}", id, path, pflags ); let full_path = self.resolve_path(&path)?; let flags = OpenFlags::from_sftp_pflags(pflags); match self.vfs.open_file(&full_path, &flags) { Ok(file) => { if self.handles.len() >= Self::MAX_HANDLES { warn!("SSH_FXP_OPEN: handle limit reached ({})", Self::MAX_HANDLES); return self.build_status_response( id, SftpStatus::SSH_FX_FAILURE, "Handle limit reached", ); } let handle_id = self.next_handle_id; self.next_handle_id += 1; let handle = SftpHandle { id: handle_id, path: full_path, handle_type: SftpHandleType::File, file: Some(file), dir_entries: None, write_mode: flags.write, }; self.handles.insert(handle_id, handle); self.build_handle_response(id, &handle_id.to_be_bytes()) } Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理SSH_FXP_CLOSE(参考OpenSSH sftp-server.c: process_close()) fn handle_close(&mut self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_CLOSE"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let handle_bytes = read_sftp_string_bytes(&mut cursor)?; let handle_id = u32::from_be_bytes([ handle_bytes[0], handle_bytes[1], handle_bytes[2], handle_bytes[3], ]); info!("SSH_FXP_CLOSE: id={}, handle={}", id, handle_id); if let Some(handle) = self.handles.remove(&handle_id) { if handle.write_mode && handle.handle_type == SftpHandleType::File { if let Some(hook) = &self.upload_hook { if let Err(e) = hook.trigger(&handle.path, &self.user_uuid) { warn!("Upload hook failed for {:?}: {}", handle.path, e); } } } self.build_status_response(id, SftpStatus::SSH_FX_OK, "File closed") } else { self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Invalid handle") } } /// 处理SSH_FXP_READ(参考OpenSSH sftp-server.c: process_read()) fn handle_read(&mut self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_READ"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let handle_bytes = read_sftp_string_bytes(&mut cursor)?; let handle_id = u32::from_be_bytes([ handle_bytes[0], handle_bytes[1], handle_bytes[2], handle_bytes[3], ]); let offset = cursor.read_u64::()?; let length = cursor.read_u32::()?; info!( "SSH_FXP_READ: id={}, handle={}, offset={}, length={}", id, handle_id, offset, length ); if let Some(handle) = self.handles.get_mut(&handle_id) { if let Some(ref mut file) = handle.file { file.seek(SeekFrom::Start(offset)) .map_err(|e| anyhow!("Seek error: {}", e))?; let max_data_size = std::cmp::min(self.maxpacket.saturating_sub(1024), Self::MAX_XFER_SIZE); let actual_length = std::cmp::min(length, max_data_size); info!( "SSH_FXP_READ limited: requested={}, actual={}", length, actual_length ); let mut buffer = vec![0u8; actual_length as usize]; match file.read(&mut buffer) { Ok(0) => self.build_status_response(id, SftpStatus::SSH_FX_EOF, "End of file"), Ok(n) => { buffer.truncate(n); self.build_data_response(id, &buffer) } Err(e) => self.build_status_from_vfs_error(id, &e), } } else { self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Not a file handle") } } else { self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Invalid handle") } } /// 处理SSH_FXP_WRITE(参考OpenSSH sftp-server.c: process_write()) fn handle_write(&mut self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_WRITE"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let handle_bytes = read_sftp_string_bytes(&mut cursor)?; let handle_id = u32::from_be_bytes([ handle_bytes[0], handle_bytes[1], handle_bytes[2], handle_bytes[3], ]); let offset = cursor.read_u64::()?; let write_data = read_sftp_string_bytes(&mut cursor)?; info!( "SSH_FXP_WRITE: id={}, handle={}, offset={}, length={}", id, handle_id, offset, write_data.len() ); if !write_data.is_empty() { let preview_len = std::cmp::min(20, write_data.len()); let preview = &write_data[0..preview_len]; debug!( "SSH_FXP_WRITE data preview (first {} bytes): {:?}", preview_len, preview ); } if let Some(handle) = self.handles.get_mut(&handle_id) { if let Some(ref mut file) = handle.file { file.seek(SeekFrom::Start(offset)) .map_err(|e| anyhow!("Seek error: {}", e))?; match file.write_all(&write_data) { Ok(_) => { file.flush().ok(); self.build_status_response(id, SftpStatus::SSH_FX_OK, "Write successful") } Err(e) => self.build_status_from_vfs_error(id, &e), } } else { self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Not a file handle") } } else { self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Invalid handle") } } /// 处理SSH_FXP_LSTAT(参考OpenSSH sftp-server.c: process_lstat()) fn handle_lstat(&self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_LSTAT"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let path = read_sftp_string(&mut cursor)?; info!("SSH_FXP_LSTAT: id={}, path={}", id, path); let full_path = self.resolve_path(&path)?; match self.vfs.lstat(&full_path) { Ok(stat) => { let attrs = SftpAttrs::from_vfs_stat(&stat); self.build_attrs_response(id, &attrs) } Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理SSH_FXP_FSTAT(参考OpenSSH sftp-server.c: process_fstat()) fn handle_fstat(&mut self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_FSTAT"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let handle_bytes = read_sftp_string_bytes(&mut cursor)?; let handle_id = u32::from_be_bytes([ handle_bytes[0], handle_bytes[1], handle_bytes[2], handle_bytes[3], ]); info!("SSH_FXP_FSTAT: id={}, handle={}", id, handle_id); if let Some(handle) = self.handles.get_mut(&handle_id) { if let Some(ref mut file) = handle.file { match file.stat() { Ok(stat) => { let attrs = SftpAttrs::from_vfs_stat(&stat); self.build_attrs_response(id, &attrs) } Err(e) => self.build_status_from_vfs_error(id, &e), } } else { match self.vfs.stat(&handle.path) { Ok(stat) => { let attrs = SftpAttrs::from_vfs_stat(&stat); self.build_attrs_response(id, &attrs) } Err(e) => self.build_status_from_vfs_error(id, &e), } } } else { self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Invalid handle") } } /// 处理SSH_FXP_OPENDIR(参考OpenSSH sftp-server.c: process_opendir()) fn handle_opendir(&mut self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_OPENDIR"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let path = read_sftp_string(&mut cursor)?; info!("SSH_FXP_OPENDIR: id={}, path={}", id, path); let full_path = self.resolve_path(&path)?; match self.vfs.read_dir(&full_path) { Ok(entries) => { if self.handles.len() >= Self::MAX_HANDLES { warn!( "SSH_FXP_OPENDIR: handle limit reached ({})", Self::MAX_HANDLES ); return self.build_status_response( id, SftpStatus::SSH_FX_FAILURE, "Handle limit reached", ); } let handle_id = self.next_handle_id; self.next_handle_id += 1; let handle = SftpHandle { id: handle_id, path: full_path, handle_type: SftpHandleType::Directory, file: None, dir_entries: Some(entries), write_mode: false, }; self.handles.insert(handle_id, handle); self.build_handle_response(id, &handle_id.to_be_bytes()) } Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理SSH_FXP_READDIR(参考OpenSSH sftp-server.c: process_readdir()) fn handle_readdir(&mut self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_READDIR"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let handle_bytes = read_sftp_string_bytes(&mut cursor)?; let handle_id = u32::from_be_bytes([ handle_bytes[0], handle_bytes[1], handle_bytes[2], handle_bytes[3], ]); info!("SSH_FXP_READDIR: id={}, handle={}", id, handle_id); if let Some(handle) = self.handles.get_mut(&handle_id) { if handle.handle_type == SftpHandleType::Directory { if let Some(ref mut dir_entries) = handle.dir_entries { if dir_entries.is_empty() { self.build_status_response(id, SftpStatus::SSH_FX_EOF, "End of directory") } else { let entries: Vec<(String, SftpAttrs)> = dir_entries .drain(..std::cmp::min(100, dir_entries.len())) .map(|entry| { let attrs = SftpAttrs::from_vfs_stat(&entry.stat); (entry.name, attrs) }) .collect(); self.build_name_response(id, entries) } } else { self.build_status_response( id, SftpStatus::SSH_FX_FAILURE, "No directory entries", ) } } else { self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Not a directory handle") } } else { self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Invalid handle") } } /// 处理SSH_FXP_REMOVE(参考OpenSSH sftp-server.c: process_remove()) fn handle_remove(&self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_REMOVE"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let path = read_sftp_string(&mut cursor)?; info!("SSH_FXP_REMOVE: id={}, path={}", id, path); let full_path = self.resolve_path(&path)?; match self.vfs.remove_file(&full_path) { Ok(_) => self.build_status_response(id, SftpStatus::SSH_FX_OK, "File removed"), Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理SSH_FXP_MKDIR(参考OpenSSH sftp-server.c: process_mkdir()) fn handle_mkdir(&self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_MKDIR"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let path = read_sftp_string(&mut cursor)?; let _attrs = read_sftp_attrs(&mut cursor)?; info!("SSH_FXP_MKDIR: id={}, path={}", id, path); let full_path = self.resolve_path(&path)?; match self.vfs.create_dir(&full_path, 0o755) { Ok(_) => self.build_status_response(id, SftpStatus::SSH_FX_OK, "Directory created"), Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理SSH_FXP_RMDIR(参考OpenSSH sftp-server.c: process_rmdir()) fn handle_rmdir(&self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_RMDIR"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let path = read_sftp_string(&mut cursor)?; info!("SSH_FXP_RMDIR: id={}, path={}", id, path); let full_path = self.resolve_path(&path)?; match self.vfs.remove_dir(&full_path) { Ok(_) => self.build_status_response(id, SftpStatus::SSH_FX_OK, "Directory removed"), Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理SSH_FXP_REALPATH(参考OpenSSH sftp-server.c: process_realpath()) fn handle_realpath(&self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_REALPATH"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let path = read_sftp_string(&mut cursor)?; info!("SSH_FXP_REALPATH: id={}, path={}", id, path); let full_path = self.resolve_path(&path)?; let name_attrs_vec = vec![(full_path.to_string_lossy().to_string(), SftpAttrs::new())]; self.build_name_response(id, name_attrs_vec) } /// 处理SSH_FXP_STAT(参考OpenSSH sftp-server.c: process_stat()) fn handle_stat(&self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_STAT"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let path = read_sftp_string(&mut cursor)?; info!("SSH_FXP_STAT: id={}, path={}", id, path); let full_path = self.resolve_path(&path)?; match self.vfs.stat(&full_path) { Ok(stat) => { let attrs = SftpAttrs::from_vfs_stat(&stat); self.build_attrs_response(id, &attrs) } Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理SSH_FXP_RENAME(参考OpenSSH sftp-server.c: process_rename()) fn handle_rename(&self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_RENAME"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let old_path = read_sftp_string(&mut cursor)?; let new_path = read_sftp_string(&mut cursor)?; info!( "SSH_FXP_RENAME: id={}, old={}, new={}", id, old_path, new_path ); let old_full_path = self.resolve_path(&old_path)?; let new_full_path = self.resolve_path(&new_path)?; match self.vfs.rename(&old_full_path, &new_full_path) { Ok(_) => self.build_status_response(id, SftpStatus::SSH_FX_OK, "Rename successful"), Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理SSH_FXP_SETSTAT(参考OpenSSH sftp-server.c: process_setstat()) fn handle_setstat(&self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_SETSTAT"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let path = read_sftp_string(&mut cursor)?; let _attrs = read_sftp_attrs(&mut cursor)?; info!("SSH_FXP_SETSTAT: id={}, path={}", id, path); self.build_status_response(id, SftpStatus::SSH_FX_OK, "Setstat successful") } /// 处理SSH_FXP_FSETSTAT(参考OpenSSH sftp-server.c: process_fsetstat()) fn handle_fsetstat(&mut self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_FSETSTAT"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let handle_bytes = read_sftp_string_bytes(&mut cursor)?; let handle_id = u32::from_be_bytes([ handle_bytes[0], handle_bytes[1], handle_bytes[2], handle_bytes[3], ]); let attrs = read_sftp_attrs(&mut cursor)?; info!( "SSH_FXP_FSETSTAT: id={}, handle={}, attrs.flags={}", id, handle_id, attrs.flags ); let handle = self.handles.get_mut(&handle_id); if handle.is_none() { return self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Invalid handle"); } let handle = handle.unwrap(); if handle.handle_type != SftpHandleType::File { return self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Not a file handle"); } let path = handle.path.clone(); if attrs.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_SIZE != 0 { if let Some(size) = attrs.size { info!("FSETSTAT: setting file size to {}", size); if let Some(ref mut file) = handle.file { file.set_len(size) .map_err(|e| anyhow!("set_len error: {}", e))?; } else { let flags = OpenFlags::new().write(); if let Ok(mut f) = self.vfs.open_file(&path, &flags) { f.set_len(size).ok(); } } } } if attrs.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_PERMISSIONS != 0 || attrs.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_ACMODTIME != 0 { let mut vfs_stat = crate::vfs::VfsStat::new(); if attrs.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_PERMISSIONS != 0 { vfs_stat.mode = attrs.permissions.unwrap_or(0); } else { if let Ok(s) = self.vfs.lstat(&path) { vfs_stat.mode = s.mode; } } if attrs.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_ACMODTIME != 0 { if let (Some(atime), Some(mtime)) = (attrs.atime, attrs.mtime) { vfs_stat.atime = std::time::UNIX_EPOCH + std::time::Duration::from_secs(atime as u64); vfs_stat.mtime = std::time::UNIX_EPOCH + std::time::Duration::from_secs(mtime as u64); } } self.vfs.set_stat(&path, &vfs_stat).ok(); } self.build_status_response(id, SftpStatus::SSH_FX_OK, "Fsetstat successful") } /// 处理SSH_FXP_READLINK(Phase 10:参考OpenSSH sftp-server.c: process_readlink()) fn handle_readlink(&self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_READLINK"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let path = read_sftp_string(&mut cursor)?; info!("SSH_FXP_READLINK: id={}, path={}", id, path); let full_path = self.resolve_path(&path)?; match self.vfs.read_link(&full_path) { Ok(link_target) => { let target = link_target.to_string_lossy().to_string(); self.build_name_response(id, vec![(target, SftpAttrs::default())]) } Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理SSH_FXP_SYMLINK(Phase 10:参考OpenSSH sftp-server.c: process_symlink()) fn handle_symlink(&self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_SYMLINK"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let linkpath = read_sftp_string(&mut cursor)?; let targetpath = read_sftp_string(&mut cursor)?; info!( "SSH_FXP_SYMLINK: id={}, link={}, target={}", id, linkpath, targetpath ); let full_linkpath = self.resolve_path(&linkpath)?; let full_targetpath = self.resolve_path(&targetpath)?; match self.vfs.create_symlink(&full_targetpath, &full_linkpath) { Ok(_) => self.build_status_response(id, SftpStatus::SSH_FX_OK, "Symlink created"), Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理SSH_FXP_EXTENDED(Phase 10:参考OpenSSH sftp-server.c: process_extended()) fn handle_extended(&mut self, data: &[u8]) -> Result> { info!("Processing SSH_FXP_EXTENDED"); let mut cursor = std::io::Cursor::new(data); cursor.set_position(1); let id = cursor.read_u32::()?; let extension_name = read_sftp_string(&mut cursor)?; info!("SSH_FXP_EXTENDED: id={}, extension={}", id, extension_name); // 支持常见的SFTP扩展 match extension_name.as_str() { "statvfs@openssh.com" => self.handle_statvfs(&mut cursor, id), "fstatvfs@openssh.com" => self.handle_fstatvfs(&mut cursor, id), "hardlink@openssh.com" => self.handle_hardlink(&mut cursor, id), "posix-rename@openssh.com" => self.handle_posix_rename(&mut cursor, id), "md5-hash@openssh.com" => self.handle_md5_hash(&mut cursor, id), "sha256-hash@openssh.com" => self.handle_sha256_hash(&mut cursor, id), "sha384-hash@openssh.com" => self.handle_sha384_hash(&mut cursor, id), "sha512-hash@openssh.com" => self.handle_sha512_hash(&mut cursor, id), "check-file@openssh.com" => self.handle_check_file(&mut cursor, id), "copy-data@openssh.com" => self.handle_copy_data(&mut cursor, id), _ => { warn!("Unsupported SFTP extension: {}", extension_name); self.build_status_response( id, SftpStatus::SSH_FX_FAILURE, &format!("Unsupported extension: {}", extension_name), ) } } } /// 处理statvfs@openssh.com扩展(文件系统统计) fn handle_statvfs(&self, cursor: &mut std::io::Cursor<&[u8]>, id: u32) -> Result> { let path = read_sftp_string(cursor)?; info!("statvfs: path={}", path); let full_path = self.resolve_path(&path)?; match self.vfs.stat(&full_path) { Ok(_) => { let mut response = Vec::new(); response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?; response.write_u32::(id)?; response.write_u64::(4096)?; response.write_u64::(4096)?; response.write_u64::(1000000)?; response.write_u64::(500000)?; response.write_u64::(500000)?; response.write_u64::(100000)?; response.write_u64::(50000)?; response.write_u64::(50000)?; response.write_u64::(0)?; response.write_u64::(0)?; response.write_u64::(255)?; self.wrap_sftp_packet(&response) } Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理fstatvfs@openssh.com扩展(文件句柄统计) fn handle_fstatvfs(&mut self, cursor: &mut std::io::Cursor<&[u8]>, id: u32) -> Result> { let handle_bytes = read_sftp_string_bytes(cursor)?; let handle_id = u32::from_be_bytes([ handle_bytes[0], handle_bytes[1], handle_bytes[2], handle_bytes[3], ]); info!("fstatvfs: handle={}", handle_id); // 简化实现:返回与statvfs相同的结果 #[cfg(unix)] { let mut response = Vec::new(); response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?; response.write_u32::(id)?; response.write_u64::(4096)?; response.write_u64::(4096)?; response.write_u64::(1000000)?; response.write_u64::(500000)?; response.write_u64::(500000)?; response.write_u64::(100000)?; response.write_u64::(50000)?; response.write_u64::(50000)?; response.write_u64::(0)?; response.write_u64::(0)?; response.write_u64::(255)?; self.wrap_sftp_packet(&response) } #[cfg(not(unix))] self.build_status_response( id, SftpStatus::SSH_FX_FAILURE, "fstatvfs not supported on non-Unix systems", ) } /// 处理hardlink@openssh.com扩展(创建硬链接) fn handle_hardlink(&self, cursor: &mut std::io::Cursor<&[u8]>, id: u32) -> Result> { let oldpath = read_sftp_string(cursor)?; let newpath = read_sftp_string(cursor)?; info!("hardlink: old={}, new={}", oldpath, newpath); let full_oldpath = self.resolve_path(&oldpath)?; let full_newpath = self.resolve_path(&newpath)?; match self.vfs.hard_link(&full_oldpath, &full_newpath) { Ok(_) => self.build_status_response(id, SftpStatus::SSH_FX_OK, "Hardlink created"), Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理posix-rename@openssh.com扩展(POSIX语义重命名) fn handle_posix_rename(&self, cursor: &mut std::io::Cursor<&[u8]>, id: u32) -> Result> { let oldpath = read_sftp_string(cursor)?; let newpath = read_sftp_string(cursor)?; info!("posix-rename: old={}, new={}", oldpath, newpath); let full_oldpath = self.resolve_path(&oldpath)?; let full_newpath = self.resolve_path(&newpath)?; match self.vfs.rename(&full_oldpath, &full_newpath) { Ok(_) => { self.build_status_response(id, SftpStatus::SSH_FX_OK, "Posix rename successful") } Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理md5-hash@openssh.com扩展(Phase 11:MD5哈希计算) fn handle_md5_hash(&self, cursor: &mut std::io::Cursor<&[u8]>, id: u32) -> Result> { let path = read_sftp_string(cursor)?; let offset = cursor.read_u64::()?; let length = cursor.read_u64::()?; info!( "md5-hash: path={}, offset={}, length={}", path, offset, length ); let actual_length = std::cmp::min(length, Self::MAX_HASH_SIZE); if actual_length < length { warn!( "md5-hash: length reduced from {} to {} (MAX_HASH_SIZE)", length, actual_length ); } let full_path = self.resolve_path(&path)?; let flags = OpenFlags::new().read(); match self.vfs.open_file(&full_path, &flags) { Ok(mut file) => { file.seek(SeekFrom::Start(offset)) .map_err(|e| anyhow!("Seek error: {}", e))?; let mut buffer = vec![0u8; actual_length as usize]; file.read_exact(&mut buffer) .map_err(|e| anyhow!("Read error: {}", e))?; let hash = md5::compute(&buffer); let hash_hex = format!("{:x}", hash); let mut response = Vec::new(); response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?; response.write_u32::(id)?; response.write_u32::(4)?; response.write_all("md5".as_bytes())?; response.write_u32::(hash_hex.len() as u32)?; response.write_all(hash_hex.as_bytes())?; self.wrap_sftp_packet(&response) } Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理sha256-hash@openssh.com扩展(Phase 11:SHA256哈希计算) fn handle_sha256_hash(&self, cursor: &mut std::io::Cursor<&[u8]>, id: u32) -> Result> { let path = read_sftp_string(cursor)?; let offset = cursor.read_u64::()?; let length = cursor.read_u64::()?; info!( "sha256-hash: path={}, offset={}, length={}", path, offset, length ); let actual_length = std::cmp::min(length, Self::MAX_HASH_SIZE); if actual_length < length { warn!( "sha256-hash: length reduced from {} to {} (MAX_HASH_SIZE)", length, actual_length ); } let full_path = self.resolve_path(&path)?; let flags = OpenFlags::new().read(); match self.vfs.open_file(&full_path, &flags) { Ok(mut file) => { file.seek(SeekFrom::Start(offset)) .map_err(|e| anyhow!("Seek error: {}", e))?; let mut buffer = vec![0u8; actual_length as usize]; file.read_exact(&mut buffer) .map_err(|e| anyhow!("Read error: {}", e))?; use sha2::{Digest, Sha256}; let mut hasher = Sha256::new(); hasher.update(&buffer); let hash = hasher.finalize(); let hash_hex = format!("{:x}", hash); let mut response = Vec::new(); response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?; response.write_u32::(id)?; response.write_u32::(6)?; response.write_all("sha256".as_bytes())?; response.write_u32::(hash_hex.len() as u32)?; response.write_all(hash_hex.as_bytes())?; self.wrap_sftp_packet(&response) } Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理sha384-hash@openssh.com扩展(Phase 12:SHA384哈希计算) fn handle_sha384_hash(&self, cursor: &mut std::io::Cursor<&[u8]>, id: u32) -> Result> { let path = read_sftp_string(cursor)?; let offset = cursor.read_u64::()?; let length = cursor.read_u64::()?; info!( "sha384-hash: path={}, offset={}, length={}", path, offset, length ); let actual_length = std::cmp::min(length, Self::MAX_HASH_SIZE); if actual_length < length { warn!( "sha384-hash: length reduced from {} to {} (MAX_HASH_SIZE)", length, actual_length ); } let full_path = self.resolve_path(&path)?; let flags = OpenFlags::new().read(); match self.vfs.open_file(&full_path, &flags) { Ok(mut file) => { file.seek(SeekFrom::Start(offset)) .map_err(|e| anyhow!("Seek error: {}", e))?; let mut buffer = vec![0u8; actual_length as usize]; file.read_exact(&mut buffer) .map_err(|e| anyhow!("Read error: {}", e))?; use sha2::{Digest, Sha384}; let mut hasher = Sha384::new(); hasher.update(&buffer); let hash = hasher.finalize(); let hash_hex = format!("{:x}", hash); let mut response = Vec::new(); response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?; response.write_u32::(id)?; response.write_u32::(6)?; response.write_all("sha384".as_bytes())?; response.write_u32::(hash_hex.len() as u32)?; response.write_all(hash_hex.as_bytes())?; self.wrap_sftp_packet(&response) } Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理sha512-hash@openssh.com扩展(Phase 12:SHA512哈希计算) fn handle_sha512_hash(&self, cursor: &mut std::io::Cursor<&[u8]>, id: u32) -> Result> { let path = read_sftp_string(cursor)?; let offset = cursor.read_u64::()?; let length = cursor.read_u64::()?; info!( "sha512-hash: path={}, offset={}, length={}", path, offset, length ); let actual_length = std::cmp::min(length, Self::MAX_HASH_SIZE); if actual_length < length { warn!( "sha512-hash: length reduced from {} to {} (MAX_HASH_SIZE)", length, actual_length ); } let full_path = self.resolve_path(&path)?; let flags = OpenFlags::new().read(); match self.vfs.open_file(&full_path, &flags) { Ok(mut file) => { file.seek(SeekFrom::Start(offset)) .map_err(|e| anyhow!("Seek error: {}", e))?; let mut buffer = vec![0u8; actual_length as usize]; file.read_exact(&mut buffer) .map_err(|e| anyhow!("Read error: {}", e))?; use sha2::{Digest, Sha512}; let mut hasher = Sha512::new(); hasher.update(&buffer); let hash = hasher.finalize(); let hash_hex = format!("{:x}", hash); let mut response = Vec::new(); response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?; response.write_u32::(id)?; response.write_u32::(6)?; response.write_all("sha512".as_bytes())?; response.write_u32::(hash_hex.len() as u32)?; response.write_all(hash_hex.as_bytes())?; self.wrap_sftp_packet(&response) } Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理check-file@openssh.com扩展(Phase 12:文件检查) fn handle_check_file(&self, cursor: &mut std::io::Cursor<&[u8]>, id: u32) -> Result> { let path = read_sftp_string(cursor)?; let _check_flags = cursor.read_u32::()?; info!("check-file: path={}", path); let full_path = self.resolve_path(&path)?; match self.vfs.stat(&full_path) { Ok(stat) => { let mut response = Vec::new(); response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?; response.write_u32::(id)?; response.write_u32::(1)?; let msg = format!("File exists, size: {}", stat.size); response.write_u32::(msg.len() as u32)?; response.write_all(msg.as_bytes())?; self.wrap_sftp_packet(&response) } Err(e) => self.build_status_from_vfs_error(id, &e), } } /// 处理copy-data@openssh.com扩展(Phase 12:服务器端复制) fn handle_copy_data( &mut self, cursor: &mut std::io::Cursor<&[u8]>, id: u32, ) -> Result> { let read_handle_bytes = read_sftp_string_bytes(cursor)?; let read_offset = cursor.read_u64::()?; let read_length = cursor.read_u64::()?; let write_handle_bytes = read_sftp_string_bytes(cursor)?; let write_offset = cursor.read_u64::()?; info!("copy-data: read_handle={:?}, read_offset={}, read_length={}, write_handle={:?}, write_offset={}", read_handle_bytes, read_offset, read_length, write_handle_bytes, write_offset); let actual_length = std::cmp::min(read_length, Self::MAX_XFER_SIZE as u64); if actual_length < read_length { warn!( "copy-data: length reduced from {} to {} (MAX_XFER_SIZE)", read_length, actual_length ); } let read_handle_id = u32::from_be_bytes([ read_handle_bytes[0], read_handle_bytes[1], read_handle_bytes[2], read_handle_bytes[3], ]); let write_handle_id = u32::from_be_bytes([ write_handle_bytes[0], write_handle_bytes[1], write_handle_bytes[2], write_handle_bytes[3], ]); let read_path = if let Some(read_handle) = self.handles.get(&read_handle_id) { read_handle.path.clone() } else { return self.build_status_response( id, SftpStatus::SSH_FX_FAILURE, "Invalid read handle", ); }; let write_path = if let Some(write_handle) = self.handles.get(&write_handle_id) { write_handle.path.clone() } else { return self.build_status_response( id, SftpStatus::SSH_FX_FAILURE, "Invalid write handle", ); }; let read_flags = OpenFlags::new().read(); let write_flags = OpenFlags::new().write(); let mut read_file = match self.vfs.open_file(&read_path, &read_flags) { Ok(f) => f, Err(e) => return self.build_status_from_vfs_error(id, &e), }; let mut write_file = match self.vfs.open_file(&write_path, &write_flags) { Ok(f) => f, Err(e) => return self.build_status_from_vfs_error(id, &e), }; read_file .seek(SeekFrom::Start(read_offset)) .map_err(|e| anyhow!("Seek error: {}", e))?; let mut buffer = vec![0u8; actual_length as usize]; read_file .read_exact(&mut buffer) .map_err(|e| anyhow!("Read error: {}", e))?; write_file .seek(SeekFrom::Start(write_offset)) .map_err(|e| anyhow!("Seek error: {}", e))?; write_file .write_all(&buffer) .map_err(|e| anyhow!("Write error: {}", e))?; write_file.flush().ok(); let mut response = Vec::new(); response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?; response.write_u32::(id)?; response.write_u64::(actual_length)?; self.wrap_sftp_packet(&response) } /// 解析路径(安全性检查,参考OpenSSH sftp-server.c: path_resolve()) /// /// 安全策略: /// - 相对路径:始终限制在 root_dir 之下 /// - 绝对路径(restrict_absolute=false):允许(用户依赖文件系统权限) /// - 绝对路径(restrict_absolute=true):限制在 root_dir 之下(chroot 模式) fn resolve_path(&self, path: &str) -> Result { info!("resolve_path: input={}, root_dir={:?}", path, self.root_dir); let full_path = if path.is_empty() || path == "." { self.root_dir.clone() } else if path.starts_with('/') { PathBuf::from(path) } else { self.root_dir.join(path) }; info!("resolve_path: full_path={:?}", full_path); let is_absolute = path.starts_with('/'); // 检查路径遍历:对相对路径始终执行,对绝对路径仅在 restrict_absolute 模式下执行 let need_check = !is_absolute || self.restrict_absolute; if need_check { if full_path.exists() { let canonical = full_path .canonicalize() .map_err(|e| anyhow!("Path canonicalize error: {}", e))?; if !canonical.starts_with(&self.root_dir) { return Err(anyhow!( "Path traversal: {:?} not under {:?}", canonical, self.root_dir )); } Ok(canonical) } else { // Pre-resolve parent directory for non-existent paths if let Some(parent) = full_path.parent() { if parent.exists() { let canonical_parent = parent .canonicalize() .map_err(|e| anyhow!("Parent canonicalize error: {}", e))?; let resolved = canonical_parent.join(full_path.file_name().unwrap_or_default()); if !resolved.starts_with(&self.root_dir) { return Err(anyhow!( "Path traversal: {:?} not under {:?}", resolved, self.root_dir )); } return Ok(resolved); } } if !full_path.starts_with(&self.root_dir) { return Err(anyhow!( "Path traversal: {:?} not under {:?}", full_path, self.root_dir )); } Ok(full_path) } } else { // Absolute path, unrestricted: canonicalize if exists, else return as-is if full_path.exists() { Ok(full_path.canonicalize()?) } else { Ok(full_path) } } } /// 构建SSH_FXP_VERSION响应,包含扩展声明(参考OpenSSH sftp-server.c: process_init()) /// /// SFTP协议格式(draft-ietf-secsh-filexfer-02): /// uint32 length /// uint8 type (SSH_FXP_VERSION = 2) /// uint32 version /// // extensions: NO count field, simply paired strings until buffer empty /// string extension_name (= uint32(len_with_nul) + data + \0) /// string extension_data (= uint32(len_with_nul) + data + \0) /// /// OpenSSH uses sshbuf_put_cstring() which includes NUL terminator. /// Client reads with sshbuf_get_cstring() which expects \0 at end. fn build_version_response(&self, version: u32) -> Result> { let mut buffer = Vec::new(); buffer.write_u8(SftpPacketType::SSH_FXP_VERSION as u8)?; buffer.write_u32::(version)?; // 扩展声明 — OpenSSH sftp-server.c: process_init() style, NO count field let extensions: &[(&str, &str)] = &[ ("posix-rename@openssh.com", "1"), ("hardlink@openssh.com", "1"), ("copy-data@openssh.com", "1"), ("check-file@openssh.com", "1"), ("statvfs@openssh.com", "2"), ("fstatvfs@openssh.com", "2"), ("md5-hash@openssh.com", "1"), ("sha256-hash@openssh.com", "1"), ("sha384-hash@openssh.com", "1"), ("sha512-hash@openssh.com", "1"), ]; for (name, data) in extensions { // sshbuf_put_cstring(buf, s) → sshbuf_put_string(buf, s, strlen(s)+1) buffer.write_u32::((name.len() + 1) as u32)?; buffer.write_all(name.as_bytes())?; buffer.write_u8(0)?; buffer.write_u32::((data.len() + 1) as u32)?; buffer.write_all(data.as_bytes())?; buffer.write_u8(0)?; } self.wrap_sftp_packet(&buffer) } /// 构建SSH_FXP_STATUS响应(参考OpenSSH sftp-server.c) fn build_status_response(&self, id: u32, status: SftpStatus, message: &str) -> Result> { let mut buffer = Vec::new(); buffer.write_u8(SftpPacketType::SSH_FXP_STATUS as u8)?; buffer.write_u32::(id)?; buffer.write_u32::(status as u32)?; buffer.write_u32::(message.len() as u32)?; buffer.write_all(message.as_bytes())?; buffer.write_u32::(0)?; self.wrap_sftp_packet(&buffer) } /// 构建SSH_FXP_HANDLE响应(参考OpenSSH sftp-server.c) fn build_handle_response(&self, id: u32, handle: &[u8]) -> Result> { let mut buffer = Vec::new(); buffer.write_u8(SftpPacketType::SSH_FXP_HANDLE as u8)?; buffer.write_u32::(id)?; buffer.write_u32::(handle.len() as u32)?; buffer.write_all(handle)?; self.wrap_sftp_packet(&buffer) } /// Phase 7: 包装SFTP packet为SSH string格式(uint32(length) + packet_type + payload) fn wrap_sftp_packet(&self, packet_data: &[u8]) -> Result> { let mut response = Vec::new(); response.write_u32::(packet_data.len() as u32)?; response.write_all(packet_data)?; Ok(response) } /// 构建SSH_FXP_DATA响应(参考OpenSSH sftp-server.c) fn build_data_response(&self, id: u32, data: &[u8]) -> Result> { let mut buffer = Vec::new(); buffer.write_u8(SftpPacketType::SSH_FXP_DATA as u8)?; buffer.write_u32::(id)?; buffer.write_u32::(data.len() as u32)?; buffer.write_all(data)?; self.wrap_sftp_packet(&buffer) } /// 构建SSH_FXP_NAME响应(参考OpenSSH sftp-server.c) fn build_name_response(&self, id: u32, entries: Vec<(String, SftpAttrs)>) -> Result> { let mut buffer = Vec::new(); buffer.write_u8(SftpPacketType::SSH_FXP_NAME as u8)?; buffer.write_u32::(id)?; buffer.write_u32::(entries.len() as u32)?; for (name, attrs) in entries { buffer.write_u32::(name.len() as u32)?; buffer.write_all(name.as_bytes())?; let long_name = name.clone(); buffer.write_u32::(long_name.len() as u32)?; buffer.write_all(long_name.as_bytes())?; buffer.write_all(&attrs.serialize()?)?; } self.wrap_sftp_packet(&buffer) } /// 构建SSH_FXP_ATTRS响应(参考OpenSSH sftp-server.c) fn build_attrs_response(&self, id: u32, attrs: &SftpAttrs) -> Result> { let mut buffer = Vec::new(); buffer.write_u8(SftpPacketType::SSH_FXP_ATTRS as u8)?; buffer.write_u32::(id)?; buffer.write_all(&attrs.serialize()?)?; self.wrap_sftp_packet(&buffer) } /// 将 std::io::Error 映射为对应的 SSH_FX_* 状态码 fn map_io_error_kind(err: &std::io::Error) -> SftpStatus { match err.kind() { std::io::ErrorKind::NotFound => SftpStatus::SSH_FX_NO_SUCH_FILE, std::io::ErrorKind::PermissionDenied => SftpStatus::SSH_FX_PERMISSION_DENIED, std::io::ErrorKind::AlreadyExists => SftpStatus::SSH_FX_FAILURE, std::io::ErrorKind::InvalidInput => SftpStatus::SSH_FX_BAD_MESSAGE, std::io::ErrorKind::WriteZero => SftpStatus::SSH_FX_FAILURE, std::io::ErrorKind::UnexpectedEof => SftpStatus::SSH_FX_EOF, _ => SftpStatus::SSH_FX_FAILURE, } } /// 根据 Error 构建状态响应(自动映射错误类型) fn build_status_from_io_error(&self, id: u32, err: &std::io::Error) -> Result> { let status = Self::map_io_error_kind(err); let msg = format!("{}", err); self.build_status_response(id, status, &msg) } /// 根据 VfsError 构建状态响应(自动映射错误类型) fn build_status_from_vfs_error(&self, id: u32, err: &crate::vfs::VfsError) -> Result> { use crate::vfs::VfsError; let status = match err { VfsError::NotFound(_) => SftpStatus::SSH_FX_NO_SUCH_FILE, VfsError::PermissionDenied(_) => SftpStatus::SSH_FX_PERMISSION_DENIED, VfsError::AlreadyExists(_) => SftpStatus::SSH_FX_FAILURE, VfsError::NotEmpty(_) => SftpStatus::SSH_FX_FAILURE, VfsError::NotADirectory(_) => SftpStatus::SSH_FX_FAILURE, VfsError::IsADirectory(_) => SftpStatus::SSH_FX_FAILURE, VfsError::Unsupported(_) => SftpStatus::SSH_FX_OP_UNSUPPORTED, VfsError::Io(_) => SftpStatus::SSH_FX_FAILURE, VfsError::UnexpectedEof => SftpStatus::SSH_FX_EOF, }; let msg = format!("{}", err); self.build_status_response(id, status, &msg) } } /// 读取SFTP字符串(参考draft-ietf-secsh-filexfer-02.txt) fn read_sftp_string(reader: &mut R) -> Result { let length = reader.read_u32::()?; let mut buffer = vec![0u8; length as usize]; reader.read_exact(&mut buffer)?; Ok(String::from_utf8(buffer)?) } /// 读取SFTP字符串字节(参考draft-ietf-secsh-filexfer-02.txt) fn read_sftp_string_bytes(reader: &mut R) -> Result> { let length = reader.read_u32::()?; let mut buffer = vec![0u8; length as usize]; reader.read_exact(&mut buffer)?; Ok(buffer) } /// 读取SFTP属性(参考draft-ietf-secsh-filexfer-02.txt) fn read_sftp_attrs(reader: &mut R) -> Result { let flags = reader.read_u32::()?; let mut attrs = SftpAttrs::new(); attrs.flags = flags; if flags & SftpAttrFlags::SSH_FILEXFER_ATTR_SIZE != 0 { attrs.size = Some(reader.read_u64::()?); } if flags & SftpAttrFlags::SSH_FILEXFER_ATTR_UIDGID != 0 { attrs.uid = Some(reader.read_u32::()?); attrs.gid = Some(reader.read_u32::()?); } if flags & SftpAttrFlags::SSH_FILEXFER_ATTR_PERMISSIONS != 0 { attrs.permissions = Some(reader.read_u32::()?); } if flags & SftpAttrFlags::SSH_FILEXFER_ATTR_ACMODTIME != 0 { attrs.atime = Some(reader.read_u32::()?); attrs.mtime = Some(reader.read_u32::()?); } if flags & SftpAttrFlags::SSH_FILEXFER_ATTR_EXTENDED != 0 { let count = reader.read_u32::()?; for _ in 0..count { let name = read_sftp_string(reader)?; let value = read_sftp_string(reader)?; attrs.extended.push((name, value)); } } Ok(attrs) } #[cfg(test)] mod tests { use super::*; use crate::vfs::local_fs::LocalFs; use std::fs::File; use tempfile::TempDir; fn make_handler(root_dir: PathBuf) -> SftpHandler { SftpHandler::new(root_dir, Box::new(LocalFs::new()), 32768, None, "test_user".to_string()) } #[test] fn test_sftp_packet_type_conversion() { assert_eq!( SftpPacketType::try_from(1).unwrap(), SftpPacketType::SSH_FXP_INIT ); assert_eq!( SftpPacketType::try_from(2).unwrap(), SftpPacketType::SSH_FXP_VERSION ); assert_eq!( SftpPacketType::try_from(3).unwrap(), SftpPacketType::SSH_FXP_OPEN ); } #[test] fn test_sftp_handler_creation() { let temp_dir = TempDir::new().unwrap(); let handler = make_handler(temp_dir.path().to_path_buf()); assert_eq!(handler.next_handle_id, 0); } #[test] fn test_sftp_attrs_from_metadata() { let temp_dir = TempDir::new().unwrap(); let file_path = temp_dir.path().join("test.txt"); File::create(&file_path).unwrap(); let metadata = fs::metadata(&file_path).unwrap(); let attrs = SftpAttrs::from_metadata(&metadata); assert!(attrs.size.is_some()); assert!(attrs.permissions.is_some()); } #[test] fn test_sftp_handle_init() { let temp_dir = TempDir::new().unwrap(); let mut handler = make_handler(temp_dir.path().to_path_buf()); // SSH_FXP_INIT packet format: type(1) + version(4) // Version 3 in big-endian: [0, 0, 0, 3] let init_packet = vec![SftpPacketType::SSH_FXP_INIT as u8, 0, 0, 0, 3]; let response = handler.handle_request(&init_packet).unwrap(); // Response format: length(4) + type(1) + version(4) + extensions // The actual SSH_FXP_VERSION is at byte 4 (after length prefix) assert!( response.len() >= 5, "Response should have length prefix + type" ); // Read length prefix let length = u32::from_be_bytes([response[0], response[1], response[2], response[3]]); assert_eq!( length as usize + 4, response.len(), "Length should match packet size" ); // Packet type should be SSH_FXP_VERSION (2) at byte 4 assert_eq!( response[4], SftpPacketType::SSH_FXP_VERSION as u8, "Packet type should be SSH_FXP_VERSION" ); } }