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

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

View File

@@ -1,12 +1,13 @@
// SCP协议实现Phase 8
// 参考OpenSSH scp.c源码
use crate::vfs::{VfsBackend, VfsFile, VfsError, VfsStat};
use crate::vfs::open_flags::OpenFlags;
use anyhow::{Result, anyhow};
use log::{info, warn, debug};
use std::path::{Path, PathBuf};
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write, BufReader, BufWriter, BufRead}; // 导入BufRead traitOpenSSH标准
use chrono::{DateTime, Utc};
use std::io::{Read, Write, BufRead};
use std::time::SystemTime;
/// SCP Handler参考OpenSSH scp.c
pub struct ScpHandler {
@@ -14,6 +15,7 @@ pub struct ScpHandler {
mode: ScpMode,
recursive: bool,
preserve_times: bool,
vfs: Box<dyn VfsBackend>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -23,24 +25,25 @@ pub enum ScpMode {
}
impl ScpHandler {
pub fn new(root_dir: PathBuf) -> Self {
pub fn new(root_dir: PathBuf, vfs: Box<dyn VfsBackend>) -> Self {
Self {
root_dir,
mode: ScpMode::Destination,
recursive: false,
preserve_times: false,
vfs,
}
}
/// 解析SCP命令参考OpenSSH scp.c: parse_command()
pub fn parse_scp_command(command: &str) -> Result<Self> {
pub fn parse_scp_command(command: &str, vfs: Box<dyn VfsBackend>) -> Result<Self> {
let parts: Vec<&str> = command.split_whitespace().collect();
if parts.len() < 2 || parts[0] != "scp" {
return Err(anyhow!("Invalid SCP command: {}", command));
}
let mut handler = ScpHandler::new(PathBuf::from("/tmp"));
let mut handler = ScpHandler::new(PathBuf::from("/tmp"), vfs);
for part in &parts[1..] {
match part {
@@ -68,19 +71,19 @@ impl ScpHandler {
/// SCP Source Modescp -f发送文件
fn handle_source_mode(&self, channel: &mut dyn ReadWrite) -> Result<()> {
info!("SCP source mode: sending files from {}", self.root_dir.display()); // 使用display()Rust标准
info!("SCP source mode: sending files from {}", self.root_dir.display());
let full_path = self.resolve_path(&self.root_dir.to_string_lossy())?;
let stat = self.vfs.stat(&full_path)
.map_err(|e| anyhow!("stat error: {}", e))?;
if full_path.is_file() {
self.send_file(channel, &full_path)?;
} else if full_path.is_dir() {
if stat.is_dir {
if !self.recursive {
return Err(anyhow!("Directory detected but -r flag not specified"));
}
self.send_directory(channel, &full_path)?;
} else {
return Err(anyhow!("Path does not exist: {}", full_path.display()));
self.send_file(channel, &full_path)?;
}
Ok(())
@@ -88,9 +91,8 @@ impl ScpHandler {
/// SCP Destination Modescp -t接收文件
fn handle_destination_mode(&mut self, channel: &mut dyn ReadWrite) -> Result<()> {
info!("SCP destination mode: receiving files to {}", self.root_dir.display()); // 使用display()Rust标准
info!("SCP destination mode: receiving files to {}", self.root_dir.display());
// 发送确认('\0'
channel.write_all(&[0])?;
channel.flush()?;
@@ -99,10 +101,9 @@ impl ScpHandler {
loop {
buffer.clear();
// 每次循环创建新的reader避免borrow冲突- OpenSSH标准
let mut reader = BufReader::new(&mut *channel);
let mut reader = std::io::BufReader::new(&mut *channel);
match reader.read_line(&mut buffer)? {
0 => break, // EOF
0 => break,
_ => {
let command = buffer.trim();
debug!("SCP command: {}", command);
@@ -113,7 +114,6 @@ impl ScpHandler {
Some('E') => self.handle_end_directory(channel)?,
Some('T') => self.handle_time_command(channel, command)?,
Some('\0') => {
// 确认信号,继续
continue;
}
_ => {
@@ -130,28 +130,30 @@ impl ScpHandler {
/// 发送文件参考OpenSSH scp.c: source()
fn send_file(&self, channel: &mut dyn ReadWrite, path: &Path) -> Result<()> {
let metadata = fs::metadata(path)?;
let size = metadata.len();
let stat = self.vfs.stat(path)
.map_err(|e| anyhow!("stat error: {}", e))?;
let size = stat.size;
let filename = path.file_name().unwrap().to_string_lossy();
// 发送文件命令C0644 size filename
let command = format!("C0644 {} {}\n", size, filename);
channel.write_all(command.as_bytes())?;
channel.flush()?;
// 等待确认('\0'
let mut ack = [0u8; 1];
channel.read_exact(&mut ack)?;
if ack[0] != 0 {
return Err(anyhow!("SCP file command rejected"));
}
// 发送文件内容
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let flags = OpenFlags::new().read();
let mut file = self.vfs.open_file(path, &flags)
.map_err(|e| anyhow!("open error: {}", e))?;
let mut buffer = vec![0u8; 8192];
while let Ok(n) = reader.read(&mut buffer) {
loop {
let n = file.read(&mut buffer)
.map_err(|e| anyhow!("read error: {}", e))?;
if n == 0 {
break;
}
@@ -160,11 +162,9 @@ impl ScpHandler {
channel.flush()?;
// 发送结束确认('\0'
channel.write_all(&[0])?;
channel.flush()?;
// 等待确认('\0'
channel.read_exact(&mut ack)?;
if ack[0] != 0 {
return Err(anyhow!("SCP file transfer rejected"));
@@ -178,35 +178,34 @@ impl ScpHandler {
fn send_directory(&self, channel: &mut dyn ReadWrite, path: &Path) -> Result<()> {
let dirname = path.file_name().unwrap().to_string_lossy();
// 发送目录命令D0755 0 dirname
let command = format!("D0755 0 {}\n", dirname);
channel.write_all(command.as_bytes())?;
channel.flush()?;
// 等待确认('\0'
let mut ack = [0u8; 1];
channel.read_exact(&mut ack)?;
if ack[0] != 0 {
return Err(anyhow!("SCP directory command rejected"));
}
// 递归发送目录内容
for entry in fs::read_dir(path)? {
let entry = entry?;
let full_path = entry.path();
let entries = self.vfs.read_dir(path)
.map_err(|e| anyhow!("read_dir error: {}", e))?;
if full_path.is_file() {
self.send_file(channel, &full_path)?;
} else if full_path.is_dir() && self.recursive {
self.send_directory(channel, &full_path)?;
for entry in &entries {
let entry_path = path.join(&entry.name);
if entry.stat.is_dir {
if self.recursive {
self.send_directory(channel, &entry_path)?;
}
} else {
self.send_file(channel, &entry_path)?;
}
}
// 发送结束目录命令E
channel.write_all("E\n".as_bytes())?;
channel.flush()?;
// 等待确认('\0'
channel.read_exact(&mut ack)?;
if ack[0] != 0 {
return Err(anyhow!("SCP end directory rejected"));
@@ -224,31 +223,25 @@ impl ScpHandler {
return self.send_error(channel, "Invalid file command format");
}
let mode = parts[0].trim_start_matches('C');
let mode_str = parts[0].trim_start_matches('C');
let size: u64 = parts[1].parse()?;
let filename = parts[2];
debug!("SCP receive file: mode={}, size={}, name={}", mode, size, filename);
debug!("SCP receive file: mode={}, size={}, name={}", mode_str, size, filename);
// 安全性检查文件大小限制防止DoS
if size > 1024 * 1024 * 1024 { // 1GB限制
if size > 1024 * 1024 * 1024 {
return self.send_error(channel, "File too large (max 1GB)");
}
// 创建文件
let full_path = self.resolve_path(filename)?;
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&full_path)?;
// 发送确认('\0'
let flags = OpenFlags::new().write().create().truncate();
let mut file = self.vfs.open_file(&full_path, &flags)
.map_err(|e| anyhow!("open error: {}", e))?;
channel.write_all(&[0])?;
channel.flush()?;
// 接收文件内容
let mut writer = BufWriter::new(file);
let mut buffer = vec![0u8; 8192];
let mut remaining = size;
@@ -258,25 +251,25 @@ impl ScpHandler {
if n == 0 {
break;
}
writer.write_all(&buffer[..n])?;
file.write_all(&buffer[..n])
.map_err(|e| anyhow!("write error: {}", e))?;
remaining -= n as u64;
}
writer.flush()?;
file.flush().map_err(|e| anyhow!("flush error: {}", e))?;
// 设置文件权限
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode_int: u32 = mode.parse()?;
fs::set_permissions(&full_path, fs::Permissions::from_mode(mode_int))?;
let mode_int: u32 = mode_str.parse()?;
if mode_int != 0 {
let mut set_stat = VfsStat::new();
set_stat.mode = mode_int;
self.vfs.set_stat(&full_path, &set_stat)
.map_err(|e| anyhow!("set_stat error: {}", e))?;
}
// 接收结束确认('\0'
let mut ack = [0u8; 1];
channel.read_exact(&mut ack)?;
// 发送确认('\0'
channel.write_all(&[0])?;
channel.flush()?;
@@ -296,24 +289,17 @@ impl ScpHandler {
return self.send_error(channel, "Recursive flag not specified");
}
let mode = parts[0].trim_start_matches('D');
let mode_str = parts[0].trim_start_matches('D');
let dirname = parts[2];
debug!("SCP receive directory: mode={}, name={}", mode, dirname);
debug!("SCP receive directory: mode={}, name={}", mode_str, dirname);
// 创建目录
let full_path = self.resolve_path(dirname)?;
fs::create_dir_all(&full_path)?;
// 设置目录权限
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode_int: u32 = mode.parse()?;
fs::set_permissions(&full_path, fs::Permissions::from_mode(mode_int))?;
}
let mode_int: u32 = mode_str.parse()?;
self.vfs.create_dir_all(&full_path, mode_int)
.map_err(|e| anyhow!("create_dir_all error: {}", e))?;
// 发送确认('\0'
channel.write_all(&[0])?;
channel.flush()?;
@@ -325,7 +311,6 @@ impl ScpHandler {
fn handle_end_directory(&self, channel: &mut dyn ReadWrite) -> Result<()> {
debug!("SCP end directory");
// 发送确认('\0'
channel.write_all(&[0])?;
channel.flush()?;
@@ -335,7 +320,6 @@ impl ScpHandler {
/// 处理时间命令T mtime atime
fn handle_time_command(&self, channel: &mut dyn ReadWrite, command: &str) -> Result<()> {
if !self.preserve_times {
// 发送确认('\0'),但不设置时间
channel.write_all(&[0])?;
channel.flush()?;
return Ok(());
@@ -347,18 +331,14 @@ impl ScpHandler {
return self.send_error(channel, "Invalid time command format");
}
let mtime: i64 = parts[1].parse()?;
let atime: i64 = parts[2].parse()?;
let mtime_secs: i64 = parts[1].parse()?;
let atime_secs: i64 = parts[2].parse()?;
debug!("SCP set times: mtime={}, atime={}", mtime, atime);
debug!("SCP set times: mtime={}, atime={}", mtime_secs, atime_secs);
// 发送确认('\0'
channel.write_all(&[0])?;
channel.flush()?;
// 时间设置将在文件接收完成后进行
// 这里仅记录实际设置在handle_file_command中
Ok(())
}
@@ -374,10 +354,13 @@ impl ScpHandler {
fn resolve_path(&self, path: &str) -> Result<PathBuf> {
let full_path = self.root_dir.join(path);
let canonical_path = full_path.canonicalize()
let canonical_path = self.vfs.real_path(&full_path)
.map_err(|e| anyhow!("Path resolution error: {}", e))?;
if !canonical_path.starts_with(&self.root_dir.canonicalize()?) {
let root_canonical = self.vfs.real_path(&self.root_dir)
.map_err(|e| anyhow!("Root path resolution error: {}", e))?;
if !canonical_path.starts_with(&root_canonical) {
return Err(anyhow!("Path traversal attempt detected"));
}
@@ -392,23 +375,28 @@ impl<T: Read + Write> ReadWrite for T {}
#[cfg(test)]
mod tests {
use super::*;
use crate::vfs::local_fs::LocalFs;
fn make_handler() -> ScpHandler {
ScpHandler::new(PathBuf::from("/tmp"), Box::new(LocalFs::new()))
}
#[test]
fn test_scp_command_parse() {
let handler = ScpHandler::parse_scp_command("scp -t /tmp").unwrap();
let handler = ScpHandler::parse_scp_command("scp -t /tmp", Box::new(LocalFs::new())).unwrap();
assert_eq!(handler.mode, ScpMode::Destination);
assert_eq!(handler.root_dir, PathBuf::from("/tmp"));
}
#[test]
fn test_scp_recursive_parse() {
let handler = ScpHandler::parse_scp_command("scp -r -t /tmp").unwrap();
let handler = ScpHandler::parse_scp_command("scp -r -t /tmp", Box::new(LocalFs::new())).unwrap();
assert!(handler.recursive);
}
#[test]
fn test_scp_source_parse() {
let handler = ScpHandler::parse_scp_command("scp -f /tmp").unwrap();
let handler = ScpHandler::parse_scp_command("scp -f /tmp", Box::new(LocalFs::new())).unwrap();
assert_eq!(handler.mode, ScpMode::Source);
}
}
}