Files
markbase/markbase-core/src/ssh_server/sftp_handler.rs
Warren e2d58538f9
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled
Implement Upload Hook for momentry integration (Phase 1)
- Add upload_hook.rs module: trigger video_probe + video_register on upload
- Add UploadHookSection to config: video extensions, binary paths
- Integrate with SFTP: handle_close triggers hook on write files
- Integrate with SCP/rsync: child process exit triggers hook
- All 155 tests pass
2026-06-19 06:26:20 +08:00

1858 lines
68 KiB
Rust
Raw Blame History

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