Files
markbase/markbase-core/src/ssh_server/sftp_handler.rs
Warren 063c0a589f
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled
Phase 1: Add detailed logging for SSH_FXP_WRITE and SSH_FXP_ATTRS
- Phase 1.2: Add SSH_FXP_WRITE data preview (first 20 bytes)
- Phase 1.3: Add SSH_FXP_ATTRS serialization debug log (flags, size, permissions, etc.)
- Improve SFTP debugging capability for future troubleshooting
- Reference: OpenSSH sftp-server.c logging style

Changes:
- sftp_handler.rs: handle_write() - add data preview debug log
- sftp_handler.rs: SftpAttrs::serialize() - add detailed field log
2026-06-17 19:36:57 +08:00

1564 lines
61 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::ssh_server::packet::{SshPacket, PacketType};
use anyhow::{Result, anyhow};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use log::{info, warn, debug};
use std::path::{Path, PathBuf};
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write, Seek, SeekFrom};
use std::os::unix::fs::PermissionsExt; // 导入PermissionsExt traitUnix标准
/// 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_PERMISSIONS
| SftpAttrFlags::SSH_FILEXFER_ATTR_ACMODTIME;
attrs.size = Some(metadata.len());
attrs.permissions = Some(metadata.permissions().mode());
if let Ok(atime) = metadata.accessed() {
attrs.atime = Some(atime.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as u32);
}
if let Ok(mtime) = metadata.modified() {
attrs.mtime = Some(mtime.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as u32);
}
attrs
}
pub fn serialize(&self) -> Vec<u8> {
// ⭐⭐⭐⭐⭐ Phase 1.3: 添加 SSH_FXP_ATTRS 详细日志
debug!("Serializing SftpAttrs: flags=0x{:08x}, size={}, uid={}, gid={}, permissions=0x{:08x}, atime={}, mtime={}",
self.flags,
self.size.unwrap_or(0),
self.uid.unwrap_or(0),
self.gid.unwrap_or(0),
self.permissions.unwrap_or(0),
self.atime.unwrap_or(0),
self.mtime.unwrap_or(0)
);
let mut buffer = Vec::new();
buffer.write_u32::<BigEndian>(self.flags).unwrap();
if self.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_SIZE != 0 {
if let Some(size) = self.size {
buffer.write_u64::<BigEndian>(size).unwrap();
}
}
if self.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_UIDGID != 0 {
if let (Some(uid), Some(gid)) = (self.uid, self.gid) {
buffer.write_u32::<BigEndian>(uid).unwrap();
buffer.write_u32::<BigEndian>(gid).unwrap();
}
}
if self.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_PERMISSIONS != 0 {
if let Some(permissions) = self.permissions {
buffer.write_u32::<BigEndian>(permissions).unwrap();
}
}
if self.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_ACMODTIME != 0 {
if let (Some(atime), Some(mtime)) = (self.atime, self.mtime) {
buffer.write_u32::<BigEndian>(atime).unwrap();
buffer.write_u32::<BigEndian>(mtime).unwrap();
}
}
if self.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_EXTENDED != 0 {
buffer.write_u32::<BigEndian>(self.extended.len() as u32).unwrap();
for (name, value) in &self.extended {
buffer.write_u32::<BigEndian>(name.len() as u32).unwrap();
buffer.write_all(name.as_bytes()).unwrap();
buffer.write_u32::<BigEndian>(value.len() as u32).unwrap();
buffer.write_all(value.as_bytes()).unwrap();
}
}
buffer
}
}
/// SFTP handle文件或目录句柄
#[derive(Debug)] // 移除CloneFile/DirEntry不支持Clone
pub struct SftpHandle {
pub id: u32,
pub path: PathBuf,
pub handle_type: SftpHandleType,
pub file: Option<File>,
pub dir_entries: Option<Vec<fs::DirEntry>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SftpHandleType {
File,
Directory,
}
/// SFTP处理管理器参考OpenSSH sftp-server.c
pub struct SftpHandler {
root_dir: PathBuf,
next_handle_id: u32,
handles: std::collections::HashMap<u32, SftpHandle>,
}
impl SftpHandler {
pub fn new(root_dir: PathBuf) -> Self {
let canonical_root = root_dir.canonicalize().unwrap_or(root_dir);
Self {
root_dir: canonical_root,
next_handle_id: 0,
handles: std::collections::HashMap::new(),
}
}
/// 处理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 file = if pflags & SftpFileFlags::SSH_FXF_READ != 0 {
OpenOptions::new().read(true).open(&full_path).ok()
} else if pflags & SftpFileFlags::SSH_FXF_WRITE != 0 {
let mut opts = OpenOptions::new();
opts.write(true);
if pflags & SftpFileFlags::SSH_FXF_APPEND != 0 {
opts.append(true);
}
if pflags & SftpFileFlags::SSH_FXF_CREAT != 0 {
opts.create(true);
}
if pflags & SftpFileFlags::SSH_FXF_TRUNC != 0 {
opts.truncate(true);
}
if pflags & SftpFileFlags::SSH_FXF_EXCL != 0 {
opts.create_new(true);
}
opts.open(&full_path).ok()
} else {
None
};
match file {
Some(file) => {
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,
};
self.handles.insert(handle_id, handle);
self.build_handle_response(id, &handle_id.to_be_bytes())
}
None => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Failed to open file")
}
}
}
/// 处理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 self.handles.remove(&handle_id).is_some() {
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))?;
let mut buffer = vec![0u8; 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_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Read error: {}", 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());
// ⭐⭐⭐⭐⭐ Phase 1.2: 添加 data preview显示前 20 字节)
if write_data.len() > 0 {
let preview_len = std::cmp::min(20, write_data.len());
let preview = &write_data[0..preview_len];
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))?;
match file.write_all(&write_data) {
Ok(_) => {
self.build_status_response(id, SftpStatus::SSH_FX_OK, "Write successful")
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Write error: {}", e))
}
}
} 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 fs::symlink_metadata(&full_path) {
Ok(metadata) => {
let attrs = SftpAttrs::from_metadata(&metadata);
self.build_attrs_response(id, &attrs)
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_NO_SUCH_FILE, &format!("Stat error: {}", 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(&handle_id) {
match fs::metadata(&handle.path) {
Ok(metadata) => {
let attrs = SftpAttrs::from_metadata(&metadata);
self.build_attrs_response(id, &attrs)
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Fstat error: {}", 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 fs::read_dir(&full_path) {
Ok(entries) => {
let handle_id = self.next_handle_id;
self.next_handle_id += 1;
let dir_entries: Vec<fs::DirEntry> = entries.filter_map(|e| e.ok()).collect();
let handle = SftpHandle {
id: handle_id,
path: full_path,
handle_type: SftpHandleType::Directory,
file: None,
dir_entries: Some(dir_entries),
};
self.handles.insert(handle_id, handle);
self.build_handle_response(id, &handle_id.to_be_bytes())
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Opendir error: {}", e))
}
}
}
/// 处理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()))
.filter_map(|entry| {
let name = entry.file_name().to_string_lossy().to_string();
let attrs = entry.metadata().ok()?;
let sftp_attrs = SftpAttrs::from_metadata(&attrs);
Some((name, sftp_attrs))
})
.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 fs::remove_file(&full_path) {
Ok(_) => {
self.build_status_response(id, SftpStatus::SSH_FX_OK, "File removed")
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Remove error: {}", e))
}
}
}
/// 处理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 fs::create_dir(&full_path) {
Ok(_) => {
self.build_status_response(id, SftpStatus::SSH_FX_OK, "Directory created")
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Mkdir error: {}", e))
}
}
}
/// 处理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 fs::remove_dir(&full_path) {
Ok(_) => {
self.build_status_response(id, SftpStatus::SSH_FX_OK, "Directory removed")
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Rmdir error: {}", e))
}
}
}
/// 处理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 fs::metadata(&full_path) {
Ok(metadata) => {
let attrs = SftpAttrs::from_metadata(&metadata);
self.build_attrs_response(id, &attrs)
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_NO_SUCH_FILE, &format!("Stat error: {}", 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 fs::rename(&old_full_path, &new_full_path) {
Ok(_) => {
self.build_status_response(id, SftpStatus::SSH_FX_OK, "Rename successful")
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Rename error: {}", e))
}
}
}
/// 处理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(&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);
let file = OpenOptions::new().write(true).open(&path)?;
file.set_len(size)?;
}
}
if attrs.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_PERMISSIONS != 0 {
if let Some(permissions) = attrs.permissions {
info!("FSETSTAT: setting permissions to {:o}", permissions);
fs::set_permissions(&path, fs::Permissions::from_mode(permissions))?;
}
}
if attrs.flags & SftpAttrFlags::SSH_FILEXFER_ATTR_ACMODTIME != 0 {
if let (Some(atime), Some(mtime)) = (attrs.atime, attrs.mtime) {
info!("FSETSTAT: setting atime={}, mtime={}", atime, mtime);
let atime_filetime = filetime::FileTime::from_unix_time(atime as i64, 0);
let mtime_filetime = filetime::FileTime::from_unix_time(mtime as i64, 0);
filetime::set_file_times(&path, atime_filetime, mtime_filetime)?;
}
}
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 fs::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_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Readlink error: {}", 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)?;
#[cfg(unix)]
match std::os::unix::fs::symlink(&full_targetpath, &full_linkpath) {
Ok(_) => {
self.build_status_response(id, SftpStatus::SSH_FX_OK, "Symlink created")
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Symlink error: {}", e))
}
}
#[cfg(not(unix))]
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Symlink not supported on non-Unix systems")
}
/// 处理SSH_FXP_EXTENDEDPhase 10参考OpenSSH sftp-server.c: process_extended())
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)?;
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
match fs::metadata(&full_path) {
Ok(metadata) => {
// 构建statvfs response参考OpenSSH sftp-server.c
let mut response = Vec::new();
response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?;
response.write_u32::<BigEndian>(id)?;
// f_bsize文件系统块大小
response.write_u64::<BigEndian>(4096)?;
// f_frsize基本块大小
response.write_u64::<BigEndian>(4096)?;
// f_blocks总块数
response.write_u64::<BigEndian>(1000000)?;
// f_bfree空闲块数
response.write_u64::<BigEndian>(500000)?;
// f_bavail可用块数
response.write_u64::<BigEndian>(500000)?;
// f_files总文件数
response.write_u64::<BigEndian>(100000)?;
// f_ffree空闲文件数
response.write_u64::<BigEndian>(50000)?;
// f_favail可用文件数
response.write_u64::<BigEndian>(50000)?;
// f_fsid文件系统ID
response.write_u64::<BigEndian>(0)?;
// f_flag标志
response.write_u64::<BigEndian>(0)?;
// f_namemax文件名最大长度
response.write_u64::<BigEndian>(255)?;
self.wrap_sftp_packet(&response)
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("statvfs error: {}", e))
}
}
}
#[cfg(not(unix))]
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "statvfs not supported on non-Unix systems")
}
/// 处理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)?;
#[cfg(unix)]
match fs::hard_link(&full_oldpath, &full_newpath) {
Ok(_) => {
self.build_status_response(id, SftpStatus::SSH_FX_OK, "Hardlink created")
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Hardlink error: {}", e))
}
}
#[cfg(not(unix))]
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Hardlink not supported on non-Unix systems")
}
/// 处理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 fs::rename(&full_oldpath, &full_newpath) {
Ok(_) => {
self.build_status_response(id, SftpStatus::SSH_FX_OK, "Posix rename successful")
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Posix rename error: {}", e))
}
}
}
/// 处理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 full_path = self.resolve_path(&path)?;
match File::open(&full_path) {
Ok(mut file) => {
file.seek(SeekFrom::Start(offset))?;
let mut buffer = vec![0u8; length as usize];
file.read_exact(&mut buffer)?;
// 计算MD5哈希
let hash = md5::compute(&buffer);
let hash_hex = format!("{:x}", hash);
// 构建响应
let mut response = Vec::new();
response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?;
response.write_u32::<BigEndian>(id)?;
// hash-algorithm (SSH string)
response.write_u32::<BigEndian>(4)?;
response.write_all("md5".as_bytes())?;
// hash-value (SSH string)
response.write_u32::<BigEndian>(hash_hex.len() as u32)?;
response.write_all(hash_hex.as_bytes())?;
self.wrap_sftp_packet(&response)
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("MD5 hash error: {}", 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 full_path = self.resolve_path(&path)?;
match File::open(&full_path) {
Ok(mut file) => {
file.seek(SeekFrom::Start(offset))?;
let mut buffer = vec![0u8; length as usize];
file.read_exact(&mut buffer)?;
// 计算SHA256哈希使用sha2 crate
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(&buffer);
let hash = hasher.finalize();
let hash_hex = format!("{:x}", hash);
// 构建响应
let mut response = Vec::new();
response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?;
response.write_u32::<BigEndian>(id)?;
// hash-algorithm (SSH string)
response.write_u32::<BigEndian>(6)?;
response.write_all("sha256".as_bytes())?;
// hash-value (SSH string)
response.write_u32::<BigEndian>(hash_hex.len() as u32)?;
response.write_all(hash_hex.as_bytes())?;
self.wrap_sftp_packet(&response)
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("SHA256 hash error: {}", 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 full_path = self.resolve_path(&path)?;
match File::open(&full_path) {
Ok(mut file) => {
file.seek(SeekFrom::Start(offset))?;
let mut buffer = vec![0u8; length as usize];
file.read_exact(&mut buffer)?;
// 计算SHA384哈希
use sha2::{Sha384, Digest};
let mut hasher = Sha384::new();
hasher.update(&buffer);
let hash = hasher.finalize();
let hash_hex = format!("{:x}", hash);
// 构建响应
let mut response = Vec::new();
response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?;
response.write_u32::<BigEndian>(id)?;
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_response(id, SftpStatus::SSH_FX_FAILURE, &format!("SHA384 hash error: {}", 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 full_path = self.resolve_path(&path)?;
match File::open(&full_path) {
Ok(mut file) => {
file.seek(SeekFrom::Start(offset))?;
let mut buffer = vec![0u8; length as usize];
file.read_exact(&mut buffer)?;
// 计算SHA512哈希
use sha2::{Sha512, Digest};
let mut hasher = Sha512::new();
hasher.update(&buffer);
let hash = hasher.finalize();
let hash_hex = format!("{:x}", hash);
// 构建响应
let mut response = Vec::new();
response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?;
response.write_u32::<BigEndian>(id)?;
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_response(id, SftpStatus::SSH_FX_FAILURE, &format!("SHA512 hash error: {}", 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={}, flags={:#x}", path, check_flags);
let full_path = self.resolve_path(&path)?;
match fs::metadata(&full_path) {
Ok(metadata) => {
// 构建响应
let mut response = Vec::new();
response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?;
response.write_u32::<BigEndian>(id)?;
// 返回文件存在和基本信息
response.write_u32::<BigEndian>(1)?; // result: 1 = file exists
let msg = format!("File exists, size: {}", metadata.len());
response.write_u32::<BigEndian>(msg.len() as u32)?;
response.write_all(msg.as_bytes())?;
self.wrap_sftp_packet(&response)
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_NO_SUCH_FILE, &format!("Check file error: {}", e))
}
}
}
/// 处理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={}",
u32::from_be_bytes([read_handle_bytes[0], read_handle_bytes[1], read_handle_bytes[2], read_handle_bytes[3]]),
read_offset, read_length,
u32::from_be_bytes([write_handle_bytes[0], write_handle_bytes[1], write_handle_bytes[2], write_handle_bytes[3]]),
write_offset);
let read_handle_id = u32::from_be_bytes([read_handle_bytes[0], read_handle_bytes[1], read_handle_bytes[2], read_handle_bytes[3]]);
let write_handle_id = u32::from_be_bytes([write_handle_bytes[0], write_handle_bytes[1], write_handle_bytes[2], write_handle_bytes[3]]);
// 获取read handle的path不可变引用
let read_path = if let Some(read_handle) = self.handles.get(&read_handle_id) {
read_handle.path.clone()
} else {
return self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Invalid read handle");
};
// 获取write handle的path不可变引用
let write_path = if let Some(write_handle) = self.handles.get(&write_handle_id) {
write_handle.path.clone()
} else {
return self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Invalid write handle");
};
// 从read_path读取数据
match File::open(&read_path) {
Ok(mut read_file) => {
read_file.seek(SeekFrom::Start(read_offset))?;
let mut buffer = vec![0u8; read_length as usize];
read_file.read_exact(&mut buffer)?;
// 写入到write_path
match OpenOptions::new().write(true).open(&write_path) {
Ok(mut write_file) => {
write_file.seek(SeekFrom::Start(write_offset))?;
write_file.write_all(&buffer)?;
// 构建响应
let mut response = Vec::new();
response.write_u8(SftpPacketType::SSH_FXP_EXTENDED_REPLY as u8)?;
response.write_u32::<BigEndian>(id)?;
// 返回复制的字节数
response.write_u64::<BigEndian>(read_length)?;
self.wrap_sftp_packet(&response)
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Write file error: {}", e))
}
}
}
Err(e) => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, &format!("Read file error: {}", e))
}
}
}
/// 解析路径安全性检查参考OpenSSH sftp-server.c: path_resolve())
fn resolve_path(&self, path: &str) -> Result<PathBuf> {
info!("resolve_path: input={}, root_dir={:?}", path, self.root_dir);
let full_path = if path.is_empty() || path == "." {
self.root_dir.clone()
} else if path.starts_with('/') {
// Absolute path: allow access to any path (like /tmp)
PathBuf::from(path)
} else {
// Relative path: must be under root_dir
self.root_dir.join(path)
};
info!("resolve_path: full_path={:?}", full_path);
// Security: Only enforce root_dir check for relative paths
// Absolute paths are allowed (user can access any path they have filesystem permissions for)
if path.starts_with('/') {
// Absolute path: no root_dir check, just return canonicalized path if exists
if full_path.exists() {
Ok(full_path.canonicalize()?)
} else {
Ok(full_path)
}
} else {
// Relative path: enforce strict root_dir confinement
if full_path.exists() {
let canonical_path = full_path.canonicalize()?;
if !canonical_path.starts_with(&self.root_dir) {
return Err(anyhow!("Path traversal attempt detected: {:?} not under {:?}", canonical_path, self.root_dir));
}
Ok(canonical_path)
} else {
if !full_path.starts_with(&self.root_dir) {
return Err(anyhow!("Path traversal attempt detected: {:?} not under {:?}", full_path, self.root_dir));
}
Ok(full_path)
}
}
}
/// 构建SSH_FXP_VERSION响应参考OpenSSH sftp-server.c
fn build_version_response(&self, version: u32) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
// SSH_FXP_VERSION packet
buffer.write_u8(SftpPacketType::SSH_FXP_VERSION as u8)?;
buffer.write_u32::<BigEndian>(version)?;
// Phase 7: SFTP packet需要SSH string格式uint32(length) + packet_type + payload
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)
}
}
/// 读取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 tempfile::TempDir;
#[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 = SftpHandler::new(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 = SftpHandler::new(temp_dir.path().to_path_buf());
let init_packet = vec![1, 0, 0, 0, 3];
let response = handler.handle_request(&init_packet).unwrap();
assert_eq!(response[0], SftpPacketType::SSH_FXP_VERSION as u8);
}
}