VFS/DataProvider/Config refactoring + SSH public key authentication
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:
@@ -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 trait(Unix标准)
|
||||
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)] // 移除Clone(File/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_EXTENDED(Phase 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();
|
||||
|
||||
Reference in New Issue
Block a user