diff --git a/markbase-core/src/ssh_server/channel.rs b/markbase-core/src/ssh_server/channel.rs index c753b09..004341b 100644 --- a/markbase-core/src/ssh_server/channel.rs +++ b/markbase-core/src/ssh_server/channel.rs @@ -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> { if let Some(channel) = self.channels.get_mut(&channel_id) { @@ -2043,6 +2098,7 @@ struct Channel { scp_input_buffer: Vec, // 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>, // Phase 17: open file for SCP receive // Phase 13.3: 端口转发相关字段 direct_tcpip: Option, // direct-tcpip channel(Remote forwarding) forwarded_tcpip: Option, // forwarded-tcpip channel(Local forwarding) diff --git a/markbase-core/src/ssh_server/scp_handler.rs b/markbase-core/src/ssh_server/scp_handler.rs index 6f94383..6da08d9 100644 --- a/markbase-core/src/ssh_server/scp_handler.rs +++ b/markbase-core/src/ssh_server/scp_handler.rs @@ -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, + pub vfs: Box, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -372,7 +372,7 @@ impl ScpHandler { } /// 路径解析(安全性检查) - fn resolve_path(&self, path: &str) -> Result { + pub fn resolve_path(&self, path: &str) -> Result { let full_path = self.root_dir.join(path); let canonical_path = self diff --git a/markbase-core/src/ssh_server/server.rs b/markbase-core/src/ssh_server/server.rs index 4daa973..df4a023 100644 --- a/markbase-core/src/ssh_server/server.rs +++ b/markbase-core/src/ssh_server/server.rs @@ -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");