VFS/DataProvider/Config refactoring + SSH public key authentication
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

Phase 1-6 of refactoring plan:
- VFS abstraction (VfsBackend trait + LocalFs + OpenFlags builder)
- DataProvider trait (SqliteProvider + PgProvider, SFTPGo-compatible)
- Config refactoring (AppConfig unified sections, env overrides)
- SSH handlers (sftp/scp/rsync) migrated to VFS + DataProvider
- SSH public key authentication (Ed25519 signature verification)
- SSH stderr → CHANNEL_EXTENDED_DATA support
- Web auth uses DataProvider instead of direct SQL
- User home directory from provider (per-user isolation)
- PostgreSQL auth provider for SFTPGo compatibility
This commit is contained in:
Warren
2026-06-18 23:35:18 +08:00
parent 83fb0de78a
commit f90e4f496c
25 changed files with 2039 additions and 612 deletions

View File

@@ -2,14 +2,16 @@
// 参考OpenSSH sftp-server.c和draft-ietf-secsh-filexfer-02.txt
use crate::ssh_server::packet::{SshPacket, PacketType};
use crate::vfs::{VfsBackend, VfsFile, VfsDirEntry};
use crate::vfs::open_flags::OpenFlags;
use anyhow::{Result, anyhow, Context};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use log::{info, warn, debug};
use std::path::{Path, PathBuf};
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write, Seek, SeekFrom};
use std::os::unix::fs::PermissionsExt; // 导入PermissionsExt traitUnix标准
use std::os::unix::fs::MetadataExt; // ⭐⭐⭐⭐⭐ Phase 2.2: 导入MetadataExt trait获取uid/gid
use std::fs;
use std::io::{SeekFrom, Write};
use std::os::unix::fs::PermissionsExt;
use std::os::unix::fs::MetadataExt;
/// SFTP packet类型参考draft-ietf-secsh-filexfer-02.txt
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -178,6 +180,30 @@ impl SftpAttrs {
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<Vec<u8>> {
debug!("Serializing SftpAttrs: flags=0x{:08x}, size={:?}, uid={:?}, gid={:?}, permissions=0x{:08x}, atime={:?}, mtime={:?}",
self.flags, self.size, self.uid, self.gid,
@@ -242,13 +268,12 @@ impl SftpAttrs {
}
/// SFTP handle文件或目录句柄
#[derive(Debug)] // 移除CloneFile/DirEntry不支持Clone
pub struct SftpHandle {
pub id: u32,
pub path: PathBuf,
pub handle_type: SftpHandleType,
pub file: Option<File>,
pub dir_entries: Option<Vec<fs::DirEntry>>,
pub file: Option<Box<dyn VfsFile>>,
pub dir_entries: Option<Vec<VfsDirEntry>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -260,6 +285,7 @@ pub enum SftpHandleType {
/// SFTP处理管理器参考OpenSSH sftp-server.c
pub struct SftpHandler {
root_dir: PathBuf,
vfs: Box<dyn VfsBackend>,
next_handle_id: u32,
handles: std::collections::HashMap<u32, SftpHandle>,
// ⭐⭐⭐⭐⭐ Phase 4: 添加 client maxpack 限制参考OpenSSH sftp-server.c
@@ -277,14 +303,15 @@ impl SftpHandler {
const MAX_HASH_SIZE: u64 = 268_435_456;
// ⭐⭐⭐⭐⭐ Phase 4: 修改 new() 方法,接受 maxpack 参数
pub fn new(root_dir: PathBuf, maxpacket: u32) -> Self {
pub fn new(root_dir: PathBuf, vfs: Box<dyn VfsBackend>, maxpacket: u32) -> 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, // 默认允许绝对路径
restrict_absolute: false,
}
}
@@ -360,30 +387,9 @@ impl SftpHandler {
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);
let file_result = if pflags & SftpFileFlags::SSH_FXF_READ != 0 {
OpenOptions::new().read(true).open(&full_path)
} else if pflags & SftpFileFlags::SSH_FXF_WRITE != 0 {
let mut opts = OpenOptions::new();
opts.write(true);
if pflags & SftpFileFlags::SSH_FXF_APPEND != 0 {
opts.append(true);
}
if pflags & SftpFileFlags::SSH_FXF_CREAT != 0 {
opts.create(true);
}
if pflags & SftpFileFlags::SSH_FXF_TRUNC != 0 {
opts.truncate(true);
}
if pflags & SftpFileFlags::SSH_FXF_EXCL != 0 {
opts.create_new(true);
}
opts.open(&full_path)
} else {
return self.build_status_response(id, SftpStatus::SSH_FX_OP_UNSUPPORTED, "Unsupported open flags");
};
match file_result {
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);
@@ -405,7 +411,7 @@ impl SftpHandler {
self.build_handle_response(id, &handle_id.to_be_bytes())
}
Err(e) => {
self.build_status_from_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
}
@@ -447,9 +453,8 @@ impl SftpHandler {
if let Some(handle) = self.handles.get_mut(&handle_id) {
if let Some(ref mut file) = handle.file {
file.seek(SeekFrom::Start(offset))?;
file.seek(SeekFrom::Start(offset)).map_err(|e| anyhow!("Seek error: {}", e))?;
// ⭐⭐⭐⭐⭐ 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);
@@ -465,7 +470,7 @@ impl SftpHandler {
self.build_data_response(id, &buffer)
}
Err(e) => {
self.build_status_from_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
} else {
@@ -491,7 +496,6 @@ impl SftpHandler {
info!("SSH_FXP_WRITE: id={}, handle={}, offset={}, length={}", id, handle_id, offset, write_data.len());
// ⭐⭐⭐⭐⭐ Phase 1.2: 添加 data preview显示前 20 字节)
if write_data.len() > 0 {
let preview_len = std::cmp::min(20, write_data.len());
let preview = &write_data[0..preview_len];
@@ -500,14 +504,15 @@ impl SftpHandler {
if let Some(handle) = self.handles.get_mut(&handle_id) {
if let Some(ref mut file) = handle.file {
file.seek(SeekFrom::Start(offset))?;
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_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
} else {
@@ -532,13 +537,13 @@ impl SftpHandler {
let full_path = self.resolve_path(&path)?;
match fs::symlink_metadata(&full_path) {
Ok(metadata) => {
let attrs = SftpAttrs::from_metadata(&metadata);
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_response(id, SftpStatus::SSH_FX_NO_SUCH_FILE, &format!("Stat error: {}", e))
self.build_status_from_vfs_error(id, &e)
}
}
}
@@ -556,14 +561,26 @@ impl SftpHandler {
info!("SSH_FXP_FSTAT: id={}, handle={}", id, handle_id);
if let Some(handle) = self.handles.get(&handle_id) {
match fs::metadata(&handle.path) {
Ok(metadata) => {
let attrs = SftpAttrs::from_metadata(&metadata);
self.build_attrs_response(id, &attrs)
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)
}
}
Err(e) => {
self.build_status_from_io_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 {
@@ -585,7 +602,7 @@ impl SftpHandler {
let full_path = self.resolve_path(&path)?;
match fs::read_dir(&full_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);
@@ -594,14 +611,12 @@ impl SftpHandler {
let handle_id = self.next_handle_id;
self.next_handle_id += 1;
let dir_entries: Vec<fs::DirEntry> = entries.filter_map(|e| e.ok()).collect();
let handle = SftpHandle {
id: handle_id,
path: full_path,
handle_type: SftpHandleType::Directory,
file: None,
dir_entries: Some(dir_entries),
dir_entries: Some(entries),
};
self.handles.insert(handle_id, handle);
@@ -609,7 +624,7 @@ impl SftpHandler {
self.build_handle_response(id, &handle_id.to_be_bytes())
}
Err(e) => {
self.build_status_from_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
}
@@ -635,11 +650,9 @@ impl SftpHandler {
} else {
let entries: Vec<(String, SftpAttrs)> = dir_entries
.drain(..std::cmp::min(100, dir_entries.len()))
.filter_map(|entry| {
let name = entry.file_name().to_string_lossy().to_string();
let attrs = entry.metadata().ok()?;
let sftp_attrs = SftpAttrs::from_metadata(&attrs);
Some((name, sftp_attrs))
.map(|entry| {
let attrs = SftpAttrs::from_vfs_stat(&entry.stat);
(entry.name, attrs)
})
.collect();
@@ -670,12 +683,12 @@ impl SftpHandler {
let full_path = self.resolve_path(&path)?;
match fs::remove_file(&full_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_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
}
@@ -695,12 +708,12 @@ impl SftpHandler {
let full_path = self.resolve_path(&path)?;
match fs::create_dir(&full_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_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
}
@@ -719,12 +732,12 @@ impl SftpHandler {
let full_path = self.resolve_path(&path)?;
match fs::remove_dir(&full_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_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
}
@@ -765,13 +778,13 @@ impl SftpHandler {
let full_path = self.resolve_path(&path)?;
match fs::metadata(&full_path) {
Ok(metadata) => {
let attrs = SftpAttrs::from_metadata(&metadata);
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_response(id, SftpStatus::SSH_FX_NO_SUCH_FILE, &format!("Stat error: {}", e))
self.build_status_from_vfs_error(id, &e)
}
}
}
@@ -792,12 +805,12 @@ impl SftpHandler {
let old_full_path = self.resolve_path(&old_path)?;
let new_full_path = self.resolve_path(&new_path)?;
match fs::rename(&old_full_path, &new_full_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_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
}
@@ -832,7 +845,7 @@ impl SftpHandler {
info!("SSH_FXP_FSETSTAT: id={}, handle={}, attrs.flags={}", id, handle_id, attrs.flags);
let handle = self.handles.get(&handle_id);
let handle = self.handles.get_mut(&handle_id);
if handle.is_none() {
return self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Invalid handle");
}
@@ -847,25 +860,35 @@ impl SftpHandler {
if attrs.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_SIZE != 0 {
if let Some(size) = attrs.size {
info!("FSETSTAT: setting file size to {}", size);
let file = OpenOptions::new().write(true).open(&path)?;
file.set_len(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 {
if let Some(permissions) = attrs.permissions {
info!("FSETSTAT: setting permissions to {:o}", permissions);
fs::set_permissions(&path, fs::Permissions::from_mode(permissions))?;
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) {
info!("FSETSTAT: setting atime={}, mtime={}", atime, mtime);
let atime_filetime = filetime::FileTime::from_unix_time(atime as i64, 0);
let mtime_filetime = filetime::FileTime::from_unix_time(mtime as i64, 0);
filetime::set_file_times(&path, atime_filetime, mtime_filetime)?;
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")
@@ -885,13 +908,13 @@ impl SftpHandler {
let full_path = self.resolve_path(&path)?;
match fs::read_link(&full_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_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
}
@@ -912,18 +935,14 @@ impl SftpHandler {
let full_linkpath = self.resolve_path(&linkpath)?;
let full_targetpath = self.resolve_path(&targetpath)?;
#[cfg(unix)]
match std::os::unix::fs::symlink(&full_targetpath, &full_linkpath) {
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_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
#[cfg(not(unix))]
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Symlink not supported on non-Unix systems")
}
/// 处理SSH_FXP_EXTENDEDPhase 10参考OpenSSH sftp-server.c: process_extended())
@@ -984,50 +1003,30 @@ impl SftpHandler {
let full_path = self.resolve_path(&path)?;
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
match fs::metadata(&full_path) {
Ok(metadata) => {
// 构建statvfs response参考OpenSSH sftp-server.c
let mut response = Vec::new();
response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?;
response.write_u32::<BigEndian>(id)?;
// f_bsize文件系统块大小
response.write_u64::<BigEndian>(4096)?;
// f_frsize基本块大小
response.write_u64::<BigEndian>(4096)?;
// f_blocks总块数
response.write_u64::<BigEndian>(1000000)?;
// f_bfree空闲块数
response.write_u64::<BigEndian>(500000)?;
// f_bavail可用块数
response.write_u64::<BigEndian>(500000)?;
// f_files总文件数
response.write_u64::<BigEndian>(100000)?;
// f_ffree空闲文件数
response.write_u64::<BigEndian>(50000)?;
// f_favail可用文件数
response.write_u64::<BigEndian>(50000)?;
// f_fsid文件系统ID
response.write_u64::<BigEndian>(0)?;
// f_flag标志
response.write_u64::<BigEndian>(0)?;
// f_namemax文件名最大长度
response.write_u64::<BigEndian>(255)?;
self.wrap_sftp_packet(&response)
}
Err(e) => {
self.build_status_from_io_error(id, &e)
}
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::<BigEndian>(id)?;
response.write_u64::<BigEndian>(4096)?;
response.write_u64::<BigEndian>(4096)?;
response.write_u64::<BigEndian>(1000000)?;
response.write_u64::<BigEndian>(500000)?;
response.write_u64::<BigEndian>(500000)?;
response.write_u64::<BigEndian>(100000)?;
response.write_u64::<BigEndian>(50000)?;
response.write_u64::<BigEndian>(50000)?;
response.write_u64::<BigEndian>(0)?;
response.write_u64::<BigEndian>(0)?;
response.write_u64::<BigEndian>(255)?;
self.wrap_sftp_packet(&response)
}
Err(e) => {
self.build_status_from_vfs_error(id, &e)
}
}
#[cfg(not(unix))]
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "statvfs not supported on non-Unix systems")
}
/// 处理fstatvfs@openssh.com扩展文件句柄统计
@@ -1073,18 +1072,14 @@ impl SftpHandler {
let full_oldpath = self.resolve_path(&oldpath)?;
let full_newpath = self.resolve_path(&newpath)?;
#[cfg(unix)]
match fs::hard_link(&full_oldpath, &full_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_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
#[cfg(not(unix))]
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Hardlink not supported on non-Unix systems")
}
/// 处理posix-rename@openssh.com扩展POSIX语义重命名
@@ -1097,12 +1092,12 @@ impl SftpHandler {
let full_oldpath = self.resolve_path(&oldpath)?;
let full_newpath = self.resolve_path(&newpath)?;
match fs::rename(&full_oldpath, &full_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_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
}
@@ -1122,34 +1117,31 @@ impl SftpHandler {
let full_path = self.resolve_path(&path)?;
match File::open(&full_path) {
let flags = OpenFlags::new().read();
match self.vfs.open_file(&full_path, &flags) {
Ok(mut file) => {
file.seek(SeekFrom::Start(offset))?;
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)?;
file.read_exact(&mut buffer).map_err(|e| anyhow!("Read error: {}", e))?;
// 计算MD5哈希
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::<BigEndian>(id)?;
// hash-algorithm (SSH string)
response.write_u32::<BigEndian>(4)?;
response.write_all("md5".as_bytes())?;
// hash-value (SSH string)
response.write_u32::<BigEndian>(hash_hex.len() as u32)?;
response.write_all(hash_hex.as_bytes())?;
self.wrap_sftp_packet(&response)
}
Err(e) => {
self.build_status_from_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
}
@@ -1169,37 +1161,34 @@ impl SftpHandler {
let full_path = self.resolve_path(&path)?;
match File::open(&full_path) {
let flags = OpenFlags::new().read();
match self.vfs.open_file(&full_path, &flags) {
Ok(mut file) => {
file.seek(SeekFrom::Start(offset))?;
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)?;
file.read_exact(&mut buffer).map_err(|e| anyhow!("Read error: {}", e))?;
// 计算SHA256哈希使用sha2 crate
use sha2::{Sha256, Digest};
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::<BigEndian>(id)?;
// hash-algorithm (SSH string)
response.write_u32::<BigEndian>(6)?;
response.write_all("sha256".as_bytes())?;
// hash-value (SSH string)
response.write_u32::<BigEndian>(hash_hex.len() as u32)?;
response.write_all(hash_hex.as_bytes())?;
self.wrap_sftp_packet(&response)
}
Err(e) => {
self.build_status_from_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
}
@@ -1219,21 +1208,20 @@ impl SftpHandler {
let full_path = self.resolve_path(&path)?;
match File::open(&full_path) {
let flags = OpenFlags::new().read();
match self.vfs.open_file(&full_path, &flags) {
Ok(mut file) => {
file.seek(SeekFrom::Start(offset))?;
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)?;
file.read_exact(&mut buffer).map_err(|e| anyhow!("Read error: {}", e))?;
// 计算SHA384哈希
use sha2::{Sha384, Digest};
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::<BigEndian>(id)?;
@@ -1247,7 +1235,7 @@ impl SftpHandler {
self.wrap_sftp_packet(&response)
}
Err(e) => {
self.build_status_from_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
}
@@ -1267,21 +1255,20 @@ impl SftpHandler {
let full_path = self.resolve_path(&path)?;
match File::open(&full_path) {
let flags = OpenFlags::new().read();
match self.vfs.open_file(&full_path, &flags) {
Ok(mut file) => {
file.seek(SeekFrom::Start(offset))?;
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)?;
file.read_exact(&mut buffer).map_err(|e| anyhow!("Read error: {}", e))?;
// 计算SHA512哈希
use sha2::{Sha512, Digest};
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::<BigEndian>(id)?;
@@ -1295,7 +1282,7 @@ impl SftpHandler {
self.wrap_sftp_packet(&response)
}
Err(e) => {
self.build_status_from_io_error(id, &e)
self.build_status_from_vfs_error(id, &e)
}
}
}
@@ -1303,30 +1290,28 @@ impl SftpHandler {
/// 处理check-file@openssh.com扩展Phase 12文件检查
fn handle_check_file(&self, cursor: &mut std::io::Cursor<&[u8]>, id: u32) -> Result<Vec<u8>> {
let path = read_sftp_string(cursor)?;
let check_flags = cursor.read_u32::<BigEndian>()?;
let _check_flags = cursor.read_u32::<BigEndian>()?;
info!("check-file: path={}, flags={:#x}", path, check_flags);
info!("check-file: path={}", path);
let full_path = self.resolve_path(&path)?;
match fs::metadata(&full_path) {
Ok(metadata) => {
// 构建响应
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::<BigEndian>(id)?;
// 返回文件存在和基本信息
response.write_u32::<BigEndian>(1)?; // result: 1 = file exists
response.write_u32::<BigEndian>(1)?;
let msg = format!("File exists, size: {}", metadata.len());
let msg = format!("File exists, size: {}", stat.size);
response.write_u32::<BigEndian>(msg.len() as u32)?;
response.write_all(msg.as_bytes())?;
self.wrap_sftp_packet(&response)
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_NO_SUCH_FILE, &format!("Check file error: {}", e))
self.build_status_from_vfs_error(id, &e)
}
}
}
@@ -1339,11 +1324,8 @@ impl SftpHandler {
let write_handle_bytes = read_sftp_string_bytes(cursor)?;
let write_offset = cursor.read_u64::<BigEndian>()?;
info!("copy-data: read_handle={}, read_offset={}, read_length={}, write_handle={}, write_offset={}",
u32::from_be_bytes([read_handle_bytes[0], read_handle_bytes[1], read_handle_bytes[2], read_handle_bytes[3]]),
read_offset, read_length,
u32::from_be_bytes([write_handle_bytes[0], write_handle_bytes[1], write_handle_bytes[2], write_handle_bytes[3]]),
write_offset);
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 {
@@ -1353,52 +1335,44 @@ impl SftpHandler {
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]]);
// 获取read handle的path不可变引用
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");
};
// 获取write handle的path不可变引用
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");
};
// 从read_path读取数据
match File::open(&read_path) {
Ok(mut read_file) => {
read_file.seek(SeekFrom::Start(read_offset))?;
let mut buffer = vec![0u8; actual_length as usize];
read_file.read_exact(&mut buffer)?;
// 写入到write_path
match OpenOptions::new().write(true).open(&write_path) {
Ok(mut write_file) => {
write_file.seek(SeekFrom::Start(write_offset))?;
write_file.write_all(&buffer)?;
// 构建响应
let mut response = Vec::new();
response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?;
response.write_u32::<BigEndian>(id)?;
// 返回复制的字节数
response.write_u64::<BigEndian>(actual_length)?;
self.wrap_sftp_packet(&response)
}
Err(e) => {
self.build_status_from_io_error(id, &e)
}
}
}
Err(e) => {
self.build_status_from_io_error(id, &e)
}
}
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::<BigEndian>(id)?;
response.write_u64::<BigEndian>(actual_length)?;
self.wrap_sftp_packet(&response)
}
/// 解析路径安全性检查参考OpenSSH sftp-server.c: path_resolve()
@@ -1608,6 +1582,24 @@ impl SftpHandler {
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<Vec<u8>> {
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
@@ -1665,8 +1657,14 @@ fn read_sftp_attrs<R: std::io::Read>(reader: &mut R) -> Result<SftpAttrs> {
#[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)
}
#[test]
fn test_sftp_packet_type_conversion() {
assert_eq!(SftpPacketType::try_from(1).unwrap(), SftpPacketType::SSH_FXP_INIT);
@@ -1677,7 +1675,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(), 32768);
let handler = make_handler(temp_dir.path().to_path_buf());
assert_eq!(handler.next_handle_id, 0);
}
@@ -1697,7 +1695,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(), 32768);
let mut handler = make_handler(temp_dir.path().to_path_buf());
let init_packet = vec![1, 0, 0, 0, 3];
let response = handler.handle_request(&init_packet).unwrap();