Phase 17: SCP over SFTP subsystem + EOF/CLOSE fixes
This commit is contained in:
@@ -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 channel(Remote forwarding)
|
||||
forwarded_tcpip: Option<ForwardedTcpipChannel>, // forwarded-tcpip channel(Local forwarding)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user