Phase 17: SCP over SFTP subsystem + EOF/CLOSE fixes
Some checks failed
Test / build (push) Has been cancelled
Test / test (push) Has been cancelled

This commit is contained in:
Warren
2026-06-20 16:31:00 +08:00
parent 56217bc9a5
commit 5b439dfbef
3 changed files with 88 additions and 16 deletions

View File

@@ -180,6 +180,7 @@ impl ChannelManager {
sftp_input_buffer: Vec::new(), // ⭐⭐⭐⭐⭐ Phase 14.2修复SFTP packet累积
scp_input_buffer: Vec::new(), // ⭐⭐⭐⭐⭐ Phase 14.4修复SCP packet累积
scp_state: ScpState::Idle, // ⭐⭐⭐⭐⭐ Phase 8.3: SCP state machine
scp_output_file: None, // Phase 17: SCP file receive
direct_tcpip: None,
forwarded_tcpip: None,
};
@@ -259,6 +260,7 @@ impl ChannelManager {
sftp_input_buffer: Vec::new(),
scp_input_buffer: Vec::new(),
scp_state: ScpState::Idle, // ⭐⭐⭐⭐⭐ Phase 8.3: SCP state machine
scp_output_file: None, // Phase 17: SCP file receive
direct_tcpip: Some(direct_tcpip),
forwarded_tcpip: None,
};
@@ -335,6 +337,7 @@ impl ChannelManager {
sftp_input_buffer: Vec::new(), // ⭐⭐⭐⭐⭐ Phase 14.2修复
scp_input_buffer: Vec::new(), // ⭐⭐⭐⭐⭐ Phase 14.4修复
scp_state: ScpState::Idle, // ⭐⭐⭐⭐⭐ Phase 8.3: SCP state machine
scp_output_file: None, // Phase 17: SCP file receive
direct_tcpip: None,
forwarded_tcpip: Some(forwarded_tcpip),
};
@@ -769,12 +772,19 @@ impl ChannelManager {
);
// ⭐⭐⭐⭐⭐ Phase 8: SCP handler (subsystem)
// ⭐⭐⭐⭐⭐ Phase 8.3: Complete SCP file transfer implementation
// Reference: OpenSSH scp.c: sink() (destination mode)
// Window Control - decrease local_window
channel.local_window -= data.len() as u32;
channel.local_consumed += data.len() as u32;
// ⭐⭐⭐⭐⭐ Phase 17: If this channel has an SFTP handler but NOT an SCP
// handler, skip the SCP state machine to avoid interpreting SFTP
// binary data as SCP text commands.
if channel.sftp_handler.is_some() && channel.scp_handler.is_none() {
// Fall through to SFTP handler code below
} else {
// ⭐⭐⭐⭐⭐ Phase 8.3: Complete SCP file transfer implementation
// Reference: OpenSSH scp.c: sink() (destination mode)
// ⭐⭐⭐⭐⭐ Phase 8.3: SCP state machine logic
match channel.scp_state.clone() {
@@ -793,6 +803,7 @@ impl ChannelManager {
match first_char {
Some('C') => {
// File command: C0644 size filename
// ⭐⭐⭐⭐⭐ Phase 17: VFS-backed file receive
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() == 3 {
let mode_str = parts[0].trim_start_matches('C');
@@ -801,34 +812,57 @@ impl ChannelManager {
info!("SCP receive file: mode={}, size={}, name={}", mode_str, size, filename);
// Open file via VFS
if let Some(ref scp_handler) = channel.scp_handler {
let resolved = scp_handler.resolve_path(filename)?;
info!("SCP resolved path: {:?}", resolved);
// Create parent directory
if let Some(parent) = resolved.parent() {
let _ = scp_handler.vfs.create_dir_all(parent, 0o755);
}
// Open file for writing
use crate::vfs::open_flags::OpenFlags;
let open_flags = {
let mut f = OpenFlags::new();
f.write = true;
f.create = true;
f.truncate = true;
f
};
let file = scp_handler.vfs.open_file(&resolved, &open_flags)?;
channel.scp_output_file = Some(file);
}
// Update state
channel.scp_state = ScpState::FileCommandReceived {
size,
filename: filename.to_string(),
remaining: size,
};
// Send ACK
response.push(0);
} else {
warn!("Invalid C command format: {}", line);
response.extend_from_slice(format!("Invalid command\n").as_bytes());
response.extend_from_slice(b"Invalid command\n");
}
}
Some('D') => {
// Directory command: D0755 0 dirname
// ⭐⭐⭐⭐⭐ Phase 17: VFS-backed directory creation
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() == 3 {
let dirname = parts[2];
info!("SCP create directory: {}", dirname);
// Create directory using VFS
// TODO: Need to get VFS from scp_handler
// For now, just send ACK
if let Some(ref scp_handler) = channel.scp_handler {
let resolved = scp_handler.resolve_path(dirname)?;
scp_handler.vfs.create_dir_all(&resolved, 0o755)?;
}
response.push(0);
} else {
warn!("Invalid D command format: {}", line);
response.extend_from_slice(format!("Invalid command\n").as_bytes());
response.extend_from_slice(b"Invalid command\n");
}
}
Some('E') => {
@@ -870,12 +904,23 @@ impl ChannelManager {
// Receive file data
let to_receive = std::cmp::min(data.len() as u64, remaining);
// TODO: Write to file using VFS
// For now, just consume the data
// ⭐⭐⭐⭐⭐ Phase 17: Write to file via VFS
if let Some(ref mut file) = channel.scp_output_file {
let write_data = &data[..to_receive as usize];
if !write_data.is_empty() {
file.write_all(write_data)?;
}
}
let new_remaining = remaining - to_receive;
if new_remaining == 0 {
info!("SCP file complete: {}", filename);
// Flush and close file
if let Some(mut file) = channel.scp_output_file.take() {
file.flush()?;
}
channel.scp_state = ScpState::Idle;
// Send final ACK
@@ -890,13 +935,14 @@ impl ChannelManager {
}
ScpState::DirectoryCreated { dirname } => {
info!("SCP in directory: {}", dirname);
// TODO: Handle directory operations
}
}
return Ok(None);
} // end of else block — SCP channels return here
// ⭐⭐⭐⭐⭐ Phase 16.5: rsync in-process handler (no child process)
// Only reached for SFTP channels (sftp_handler.is_some() && scp_handler.is_none())
if let Some(rsync_handler) = &mut channel.rsync_handler {
info!(
"⭐⭐⭐⭐⭐ [RSYNC_DATA] Feeding {} bytes to RsyncHandler",
@@ -1123,7 +1169,11 @@ impl ChannelManager {
info!("Channel close: channel={}", recipient_channel);
// 移除channel参考OpenSSH channel.c
if let Some(channel) = self.channels.remove(&recipient_channel) {
if let Some(mut channel) = self.channels.remove(&recipient_channel) {
// Phase 17: Flush and close SCP output file
if let Some(mut file) = channel.scp_output_file.take() {
let _ = file.flush();
}
info!("Channel {} removed", recipient_channel);
// 发送SSH_MSG_CHANNEL_CLOSE回应
@@ -1288,6 +1338,11 @@ impl ChannelManager {
}
}
/// ⭐⭐⭐⭐⭐ Phase 17: Check if a specific channel has an exec process
pub fn channel_has_exec_process(&self, channel_id: u32) -> bool {
self.channels.get(&channel_id).map_or(false, |ch| ch.exec_process.is_some())
}
/// 获取channel输出Phase 6新增
pub fn get_channel_output(&mut self, channel_id: u32) -> Option<Vec<u8>> {
if let Some(channel) = self.channels.get_mut(&channel_id) {
@@ -2043,6 +2098,7 @@ struct Channel {
scp_input_buffer: Vec<u8>, // Phase 14.4修复累积不完整的SCP packets
// ⭐⭐⭐⭐⭐ Phase 8.3: SCP file transfer state machine
scp_state: ScpState, // Phase 8.3: SCP state tracking
scp_output_file: Option<Box<dyn crate::vfs::VfsFile>>, // Phase 17: open file for SCP receive
// Phase 13.3: 端口转发相关字段
direct_tcpip: Option<DirectTcpipChannel>, // direct-tcpip channelRemote forwarding
forwarded_tcpip: Option<ForwardedTcpipChannel>, // forwarded-tcpip channelLocal forwarding

View File

@@ -10,11 +10,11 @@ use std::path::{Path, PathBuf};
/// SCP Handler参考OpenSSH scp.c
pub struct ScpHandler {
root_dir: PathBuf,
pub root_dir: PathBuf,
mode: ScpMode,
recursive: bool,
preserve_times: bool,
vfs: Box<dyn VfsBackend>,
pub vfs: Box<dyn VfsBackend>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -372,7 +372,7 @@ impl ScpHandler {
}
/// 路径解析(安全性检查)
fn resolve_path(&self, path: &str) -> Result<PathBuf> {
pub fn resolve_path(&self, path: &str) -> Result<PathBuf> {
let full_path = self.root_dir.join(path);
let canonical_path = self

View File

@@ -655,6 +655,22 @@ fn handle_ssh_service_loop(
// Phase 17: EOF means client won't send more data → close child stdin
// (Essential for SCP upload where scp -t waits for EOF on stdin)
channel_manager.close_child_stdin();
// ⭐⭐⭐⭐⭐ Phase 17: Send SSH_MSG_CHANNEL_CLOSE in response to EOF
// ONLY for subsystem channels (no exec_process) — RFC 4254 §5.3
// For exec channels, wait for child exit → exit-status + EOF + CLOSE
let has_exec = packet.payload.len() >= 5 && {
let channel_id =
u32::from_be_bytes([packet.payload[1], packet.payload[2], packet.payload[3], packet.payload[4]]);
channel_manager.channel_has_exec_process(channel_id)
};
if !has_exec && packet.payload.len() >= 5 {
let channel_id =
u32::from_be_bytes([packet.payload[1], packet.payload[2], packet.payload[3], packet.payload[4]]);
let close_packet = channel_manager.build_channel_close(channel_id)?;
let encrypted_response =
EncryptedPacket::new(&close_packet.payload, encryption_ctx, true)?;
encrypted_response.write(stream)?;
}
}
Some(&pt) if pt == PacketType::SSH_MSG_DISCONNECT as u8 => {
info!("Received SSH_MSG_DISCONNECT");