- 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
1858 lines
68 KiB
Rust
1858 lines
68 KiB
Rust
// 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_READLINK(Phase 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_SYMLINK(Phase 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_EXTENDED(Phase 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 11:MD5哈希计算)
|
||
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 11:SHA256哈希计算)
|
||
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 12:SHA384哈希计算)
|
||
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 12:SHA512哈希计算)
|
||
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"
|
||
);
|
||
}
|
||
}
|