From 1d81db3af500086cb40e8d4641b51f5b0b43baa8 Mon Sep 17 00:00:00 2001 From: Warren Date: Thu, 18 Jun 2026 06:42:33 +0800 Subject: [PATCH] Enterprise-grade SFTP reliability improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove all unwrap() calls from SftpAttrs::serialize() and from_metadata() - Add extension advertisement in SSH_FXP_VERSION (10 extensions declared) - Map std::io::ErrorKind to proper SSH_FX_* status codes (NotFound→FX_NO_SUCH_FILE etc.) - Add restrict_absolute flag for chroot-like path confinement mode - Add MAX_HANDLES limit (4096) to prevent handle exhaustion - Add MAX_XFER_SIZE (1MB) and MAX_HASH_SIZE (256MB) OOM protection - Fix test compilation errors (SftpHandler::new signature) - Add build_status_from_io_error() helper for consistent error mapping --- markbase-core/src/ssh_server/sftp_handler.rs | 301 +++++++++++++------ 1 file changed, 214 insertions(+), 87 deletions(-) diff --git a/markbase-core/src/ssh_server/sftp_handler.rs b/markbase-core/src/ssh_server/sftp_handler.rs index 69aeffb..113921b 100644 --- a/markbase-core/src/ssh_server/sftp_handler.rs +++ b/markbase-core/src/ssh_server/sftp_handler.rs @@ -2,7 +2,7 @@ // 参考OpenSSH sftp-server.c和draft-ietf-secsh-filexfer-02.txt use crate::ssh_server::packet::{SshPacket, PacketType}; -use anyhow::{Result, anyhow}; +use anyhow::{Result, anyhow, Context}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use log::{info, warn, debug}; use std::path::{Path, PathBuf}; @@ -155,83 +155,89 @@ impl SftpAttrs { pub fn from_metadata(metadata: &fs::Metadata) -> Self { let mut attrs = Self::new(); - // ⭐⭐⭐⭐⭐ Phase 2.2: 添加 uid/gid 字段(修复 "? 0 0" 权限显示问题) attrs.flags = SftpAttrFlags::SSH_FILEXFER_ATTR_SIZE - | SftpAttrFlags::SSH_FILEXFER_ATTR_UIDGID // ⭐⭐⭐⭐⭐ 添加 UIDGID flag + | 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()); - - // ⭐⭐⭐⭐⭐ Phase 2.2: 获取 uid/gid(参考OpenSSH sftp-server.c: stat_to_attrib) attrs.uid = Some(metadata.uid()); attrs.gid = Some(metadata.gid()); if let Ok(atime) = metadata.accessed() { - attrs.atime = Some(atime.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as u32); + 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 = Some(mtime.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as u32); + attrs.mtime = mtime.duration_since(std::time::UNIX_EPOCH) + .ok().map(|d| d.as_secs() as u32); } attrs } - pub fn serialize(&self) -> Vec { - // ⭐⭐⭐⭐⭐ Phase 1.3: 添加 SSH_FXP_ATTRS 详细日志 - debug!("Serializing SftpAttrs: flags=0x{:08x}, size={}, uid={}, gid={}, permissions=0x{:08x}, atime={}, mtime={}", - self.flags, - self.size.unwrap_or(0), - self.uid.unwrap_or(0), - self.gid.unwrap_or(0), + 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.unwrap_or(0), - self.mtime.unwrap_or(0) + self.atime, self.mtime, ); let mut buffer = Vec::new(); - buffer.write_u32::(self.flags).unwrap(); + 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).unwrap(); + 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).unwrap(); - buffer.write_u32::(gid).unwrap(); + 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).unwrap(); + 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).unwrap(); - buffer.write_u32::(mtime).unwrap(); + 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).unwrap(); + 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).unwrap(); - buffer.write_all(name.as_bytes()).unwrap(); - buffer.write_u32::(value.len() as u32).unwrap(); - buffer.write_all(value.as_bytes()).unwrap(); + 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")?; } } - buffer + Ok(buffer) } } @@ -258,9 +264,18 @@ pub struct SftpHandler { handles: std::collections::HashMap, // ⭐⭐⭐⭐⭐ Phase 4: 添加 client maxpack 限制(参考OpenSSH sftp-server.c) maxpacket: u32, // 来自 SSH_MSG_CHANNEL_OPEN_CONFIRMATION 的 maximum_packet_size + /// 限制绝对路径也在 root_dir 之下(chroot 模式) + restrict_absolute: bool, } 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, maxpacket: u32) -> Self { let canonical_root = root_dir.canonicalize().unwrap_or(root_dir); @@ -268,10 +283,16 @@ impl SftpHandler { root_dir: canonical_root, next_handle_id: 0, handles: std::collections::HashMap::new(), - maxpacket, // ⭐⭐⭐⭐⭐ Phase 4: client maxpack 限制 + maxpacket, + restrict_absolute: false, // 默认允许绝对路径 } } + /// 设置 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() { @@ -320,6 +341,21 @@ impl SftpHandler { let version = cursor.read_u32::()?; info!("Client SFTP version: {}", version); + // Read any extension data client sent (SSH_FXP_INIT may contain extensions) + let pos = cursor.position() as usize; + let inner = cursor.get_ref(); + if inner.len() > pos && (inner.len() - pos) >= 4 { + let ext_count = match cursor.read_u32::() { + Ok(n) => n, + Err(_) => 0, + }; + for i in 0..ext_count { + let ext_name = read_sftp_string(&mut cursor).unwrap_or_default(); + let ext_data = read_sftp_string(&mut cursor).unwrap_or_default(); + debug!("Client extension[{}]: {} = {}", i, ext_name, ext_data); + } + } + let response = self.build_version_response(3)?; Ok(response) } @@ -364,6 +400,10 @@ impl SftpHandler { match file { Some(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; @@ -424,10 +464,8 @@ impl SftpHandler { if let Some(ref mut file) = handle.file { file.seek(SeekFrom::Start(offset))?; - // ⭐⭐⭐⭐⭐ Phase 4: 限制数据大小,不超过 maxpacket - 1024(参考OpenSSH sftp-server.c) - // OpenSSH sftp-server.c: process_read() 中限制数据大小为 maxpacket - 1024 bytes - // SSH packet overhead: ~1024 bytes (SSH header + SSH_FXP_DATA header) - let max_data_size = self.maxpacket - 1024; + // ⭐⭐⭐⭐⭐ Phase 4: 限制数据大小,不超过 maxpacket - 1024 和 MAX_XFER_SIZE + 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); @@ -442,7 +480,7 @@ impl SftpHandler { self.build_data_response(id, &buffer) } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Read error: {}", e)) + self.build_status_from_io_error(id, &e) } } } else { @@ -484,7 +522,7 @@ impl SftpHandler { self.build_status_response(id, SftpStatus::SSH_FX_OK, "Write successful") } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Write error: {}", e)) + self.build_status_from_io_error(id, &e) } } } else { @@ -540,7 +578,7 @@ impl SftpHandler { self.build_attrs_response(id, &attrs) } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Fstat error: {}", e)) + self.build_status_from_io_error(id, &e) } } } else { @@ -564,6 +602,10 @@ impl SftpHandler { match fs::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; @@ -582,7 +624,7 @@ impl SftpHandler { self.build_handle_response(id, &handle_id.to_be_bytes()) } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Opendir error: {}", e)) + self.build_status_from_io_error(id, &e) } } } @@ -648,7 +690,7 @@ impl SftpHandler { self.build_status_response(id, SftpStatus::SSH_FX_OK, "File removed") } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Remove error: {}", e)) + self.build_status_from_io_error(id, &e) } } } @@ -673,7 +715,7 @@ impl SftpHandler { self.build_status_response(id, SftpStatus::SSH_FX_OK, "Directory created") } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Mkdir error: {}", e)) + self.build_status_from_io_error(id, &e) } } } @@ -697,7 +739,7 @@ impl SftpHandler { self.build_status_response(id, SftpStatus::SSH_FX_OK, "Directory removed") } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Rmdir error: {}", e)) + self.build_status_from_io_error(id, &e) } } } @@ -770,7 +812,7 @@ impl SftpHandler { self.build_status_response(id, SftpStatus::SSH_FX_OK, "Rename successful") } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Rename error: {}", e)) + self.build_status_from_io_error(id, &e) } } } @@ -864,7 +906,7 @@ impl SftpHandler { self.build_name_response(id, vec![(target, SftpAttrs::default())]) } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Readlink error: {}", e)) + self.build_status_from_io_error(id, &e) } } } @@ -891,7 +933,7 @@ impl SftpHandler { self.build_status_response(id, SftpStatus::SSH_FX_OK, "Symlink created") } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Symlink error: {}", e)) + self.build_status_from_io_error(id, &e) } } @@ -994,7 +1036,7 @@ impl SftpHandler { self.wrap_sftp_packet(&response) } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("statvfs error: {}", e)) + self.build_status_from_io_error(id, &e) } } } @@ -1052,7 +1094,7 @@ impl SftpHandler { self.build_status_response(id, SftpStatus::SSH_FX_OK, "Hardlink created") } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Hardlink error: {}", e)) + self.build_status_from_io_error(id, &e) } } @@ -1075,7 +1117,7 @@ impl SftpHandler { self.build_status_response(id, SftpStatus::SSH_FX_OK, "Posix rename successful") } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Posix rename error: {}", e)) + self.build_status_from_io_error(id, &e) } } } @@ -1088,13 +1130,18 @@ impl SftpHandler { 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)?; match File::open(&full_path) { Ok(mut file) => { file.seek(SeekFrom::Start(offset))?; - let mut buffer = vec![0u8; length as usize]; + let mut buffer = vec![0u8; actual_length as usize]; file.read_exact(&mut buffer)?; // 计算MD5哈希 @@ -1117,7 +1164,7 @@ impl SftpHandler { self.wrap_sftp_packet(&response) } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("MD5 hash error: {}", e)) + self.build_status_from_io_error(id, &e) } } } @@ -1130,13 +1177,18 @@ impl SftpHandler { 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)?; match File::open(&full_path) { Ok(mut file) => { file.seek(SeekFrom::Start(offset))?; - let mut buffer = vec![0u8; length as usize]; + let mut buffer = vec![0u8; actual_length as usize]; file.read_exact(&mut buffer)?; // 计算SHA256哈希(使用sha2 crate) @@ -1162,7 +1214,7 @@ impl SftpHandler { self.wrap_sftp_packet(&response) } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("SHA256 hash error: {}", e)) + self.build_status_from_io_error(id, &e) } } } @@ -1175,13 +1227,18 @@ impl SftpHandler { 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)?; match File::open(&full_path) { Ok(mut file) => { file.seek(SeekFrom::Start(offset))?; - let mut buffer = vec![0u8; length as usize]; + let mut buffer = vec![0u8; actual_length as usize]; file.read_exact(&mut buffer)?; // 计算SHA384哈希 @@ -1205,7 +1262,7 @@ impl SftpHandler { self.wrap_sftp_packet(&response) } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("SHA384 hash error: {}", e)) + self.build_status_from_io_error(id, &e) } } } @@ -1218,13 +1275,18 @@ impl SftpHandler { 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)?; match File::open(&full_path) { Ok(mut file) => { file.seek(SeekFrom::Start(offset))?; - let mut buffer = vec![0u8; length as usize]; + let mut buffer = vec![0u8; actual_length as usize]; file.read_exact(&mut buffer)?; // 计算SHA512哈希 @@ -1248,7 +1310,7 @@ impl SftpHandler { self.wrap_sftp_packet(&response) } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("SHA512 hash error: {}", e)) + self.build_status_from_io_error(id, &e) } } } @@ -1298,6 +1360,11 @@ impl SftpHandler { u32::from_be_bytes([write_handle_bytes[0], write_handle_bytes[1], write_handle_bytes[2], write_handle_bytes[3]]), 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]]); @@ -1319,7 +1386,7 @@ impl SftpHandler { match File::open(&read_path) { Ok(mut read_file) => { read_file.seek(SeekFrom::Start(read_offset))?; - let mut buffer = vec![0u8; read_length as usize]; + let mut buffer = vec![0u8; actual_length as usize]; read_file.read_exact(&mut buffer)?; // 写入到write_path @@ -1334,72 +1401,112 @@ impl SftpHandler { response.write_u32::(id)?; // 返回复制的字节数 - response.write_u64::(read_length)?; + response.write_u64::(actual_length)?; self.wrap_sftp_packet(&response) } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Write file error: {}", e)) + self.build_status_from_io_error(id, &e) } } } Err(e) => { - self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Read file error: {}", e)) + self.build_status_from_io_error(id, &e) } } } - /// 解析路径(安全性检查,参考OpenSSH sftp-server.c: path_resolve()) + /// 解析路径(安全性检查,参考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('/') { - // Absolute path: allow access to any path (like /tmp) PathBuf::from(path) } else { - // Relative path: must be under root_dir self.root_dir.join(path) }; info!("resolve_path: full_path={:?}", full_path); - // Security: Only enforce root_dir check for relative paths - // Absolute paths are allowed (user can access any path they have filesystem permissions for) - if path.starts_with('/') { - // Absolute path: no root_dir check, just return canonicalized path if exists + 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) } - } else { - // Relative path: enforce strict root_dir confinement - if full_path.exists() { - let canonical_path = full_path.canonicalize()?; - if !canonical_path.starts_with(&self.root_dir) { - return Err(anyhow!("Path traversal attempt detected: {:?} not under {:?}", canonical_path, self.root_dir)); - } - Ok(canonical_path) - } else { - if !full_path.starts_with(&self.root_dir) { - return Err(anyhow!("Path traversal attempt detected: {:?} not under {:?}", full_path, self.root_dir)); - } - Ok(full_path) - } } } -/// 构建SSH_FXP_VERSION响应(参考OpenSSH sftp-server.c) +/// 构建SSH_FXP_VERSION响应,包含扩展声明(参考OpenSSH sftp-server.c) fn build_version_response(&self, version: u32) -> Result> { let mut buffer = Vec::new(); - // SSH_FXP_VERSION packet buffer.write_u8(SftpPacketType::SSH_FXP_VERSION as u8)?; buffer.write_u32::(version)?; - // Phase 7: SFTP packet需要SSH string格式(uint32(length) + packet_type + payload) + // 扩展声明(OpenSSH sftp-server.c: process_init() 中声明支持的扩展) + 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"), + ]; + + buffer.write_u32::(extensions.len() as u32)?; + for (name, data) in extensions { + buffer.write_u32::(name.len() as u32)?; + buffer.write_all(name.as_bytes())?; + buffer.write_u32::(data.len() as u32)?; + buffer.write_all(data.as_bytes())?; + } + self.wrap_sftp_packet(&buffer) } @@ -1468,7 +1575,7 @@ impl SftpHandler { buffer.write_u32::(long_name.len() as u32)?; buffer.write_all(long_name.as_bytes())?; - buffer.write_all(&attrs.serialize())?; + buffer.write_all(&attrs.serialize()?)?; } self.wrap_sftp_packet(&buffer) @@ -1480,10 +1587,30 @@ impl SftpHandler { buffer.write_u8(SftpPacketType::SSH_FXP_ATTRS as u8)?; buffer.write_u32::(id)?; - buffer.write_all(&attrs.serialize())?; + 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) + } } /// 读取SFTP字符串(参考draft-ietf-secsh-filexfer-02.txt) @@ -1553,7 +1680,7 @@ mod tests { #[test] fn test_sftp_handler_creation() { let temp_dir = TempDir::new().unwrap(); - let handler = SftpHandler::new(temp_dir.path().to_path_buf()); + let handler = SftpHandler::new(temp_dir.path().to_path_buf(), 32768); assert_eq!(handler.next_handle_id, 0); } @@ -1573,7 +1700,7 @@ mod tests { #[test] fn test_sftp_handle_init() { let temp_dir = TempDir::new().unwrap(); - let mut handler = SftpHandler::new(temp_dir.path().to_path_buf()); + let mut handler = SftpHandler::new(temp_dir.path().to_path_buf(), 32768); let init_packet = vec![1, 0, 0, 0, 3]; let response = handler.handle_request(&init_packet).unwrap();