VFS/DataProvider/Config refactoring + SSH public key authentication
Phase 1-6 of refactoring plan: - VFS abstraction (VfsBackend trait + LocalFs + OpenFlags builder) - DataProvider trait (SqliteProvider + PgProvider, SFTPGo-compatible) - Config refactoring (AppConfig unified sections, env overrides) - SSH handlers (sftp/scp/rsync) migrated to VFS + DataProvider - SSH public key authentication (Ed25519 signature verification) - SSH stderr → CHANNEL_EXTENDED_DATA support - Web auth uses DataProvider instead of direct SQL - User home directory from provider (per-user isolation) - PostgreSQL auth provider for SFTPGo compatibility
This commit is contained in:
@@ -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 trait(OpenSSH标准)
|
||||
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 Mode(scp -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 Mode(scp -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user