Enterprise-grade SFTP reliability improvements
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

- 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
This commit is contained in:
Warren
2026-06-18 06:42:33 +08:00
parent 5344a7c16e
commit 1d81db3af5

View File

@@ -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<u8> {
// ⭐⭐⭐⭐⭐ 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<Vec<u8>> {
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::<BigEndian>(self.flags).unwrap();
buffer.write_u32::<BigEndian>(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::<BigEndian>(size).unwrap();
buffer.write_u64::<BigEndian>(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::<BigEndian>(uid).unwrap();
buffer.write_u32::<BigEndian>(gid).unwrap();
buffer.write_u32::<BigEndian>(uid)
.with_context(|| "serialize attrs uid")?;
buffer.write_u32::<BigEndian>(gid)
.with_context(|| "serialize attrs gid")?;
}
}
if self.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_PERMISSIONS != 0 {
if let Some(permissions) = self.permissions {
buffer.write_u32::<BigEndian>(permissions).unwrap();
buffer.write_u32::<BigEndian>(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::<BigEndian>(atime).unwrap();
buffer.write_u32::<BigEndian>(mtime).unwrap();
buffer.write_u32::<BigEndian>(atime)
.with_context(|| "serialize attrs atime")?;
buffer.write_u32::<BigEndian>(mtime)
.with_context(|| "serialize attrs mtime")?;
}
}
if self.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_EXTENDED != 0 {
buffer.write_u32::<BigEndian>(self.extended.len() as u32).unwrap();
buffer.write_u32::<BigEndian>(self.extended.len() as u32)
.with_context(|| "serialize attrs ext count")?;
for (name, value) in &self.extended {
buffer.write_u32::<BigEndian>(name.len() as u32).unwrap();
buffer.write_all(name.as_bytes()).unwrap();
buffer.write_u32::<BigEndian>(value.len() as u32).unwrap();
buffer.write_all(value.as_bytes()).unwrap();
buffer.write_u32::<BigEndian>(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::<BigEndian>(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<u32, SftpHandle>,
// ⭐⭐⭐⭐⭐ 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<Vec<u8>> {
if data.is_empty() {
@@ -320,6 +341,21 @@ impl SftpHandler {
let version = cursor.read_u32::<BigEndian>()?;
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::<BigEndian>() {
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::<BigEndian>(id)?;
// 返回复制的字节数
response.write_u64::<BigEndian>(read_length)?;
response.write_u64::<BigEndian>(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<PathBuf> {
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<Vec<u8>> {
let mut buffer = Vec::new();
// SSH_FXP_VERSION packet
buffer.write_u8(SftpPacketType::SSH_FXP_VERSION as u8)?;
buffer.write_u32::<BigEndian>(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::<BigEndian>(extensions.len() as u32)?;
for (name, data) in extensions {
buffer.write_u32::<BigEndian>(name.len() as u32)?;
buffer.write_all(name.as_bytes())?;
buffer.write_u32::<BigEndian>(data.len() as u32)?;
buffer.write_all(data.as_bytes())?;
}
self.wrap_sftp_packet(&buffer)
}
@@ -1468,7 +1575,7 @@ impl SftpHandler {
buffer.write_u32::<BigEndian>(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::<BigEndian>(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<Vec<u8>> {
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();