-
Notifications
You must be signed in to change notification settings - Fork 1
Closed
Labels
priority:mediumMedium priority issueMedium priority issuestatus:doneCompletedCompletedtype:enhancementNew feature or requestNew feature or request
Description
Summary
Implement the SCP (Secure Copy Protocol) server to enable scp command file transfers. SCP runs as a regular command and uses a simple protocol over stdin/stdout.
Parent Epic
- Implement bssh-server with SFTP/SCP support #123 - bssh-server 추가 구현
- Depends on: Implement command execution handler for server #128 (command execution)
Background
SCP protocol is simpler than SFTP - it's essentially the scp program communicating over SSH. When a client runs scp file user@host:path, the SSH server receives an exec request for scp -t path (upload) or scp -f path (download).
Implementation Details
1. SCP Protocol Handler
// src/server/scp.rs
use tokio::io::{AsyncReadExt, AsyncWriteExt};
/// SCP mode
#[derive(Debug, Clone, Copy)]
pub enum ScpMode {
/// Source mode (-f): Server sends files to client
Source,
/// Sink mode (-t): Server receives files from client
Sink,
}
/// SCP server handler
pub struct ScpHandler {
mode: ScpMode,
path: PathBuf,
recursive: bool,
preserve_times: bool,
user_info: UserInfo,
root_dir: PathBuf,
audit: Option<Arc<dyn AuditExporter>>,
filter: Option<Arc<dyn TransferFilter>>,
}
impl ScpHandler {
/// Parse SCP command arguments
pub fn from_command(command: &str, user_info: UserInfo, config: &ScpConfig) -> Result<Self> {
// Parse: scp [-r] [-p] [-d] (-t|-f) path
let args: Vec<&str> = command.split_whitespace().collect();
if args.is_empty() || args[0] != "scp" {
anyhow::bail!("Not an SCP command");
}
let mut mode = None;
let mut recursive = false;
let mut preserve_times = false;
let mut path = None;
let mut i = 1;
while i < args.len() {
match args[i] {
"-t" => mode = Some(ScpMode::Sink),
"-f" => mode = Some(ScpMode::Source),
"-r" => recursive = true,
"-p" => preserve_times = true,
"-d" => {} // Target is directory, ignored
arg if !arg.starts_with('-') => {
path = Some(PathBuf::from(arg));
}
_ => {} // Ignore unknown options
}
i += 1;
}
let mode = mode.ok_or_else(|| anyhow::anyhow!("Missing -t or -f flag"))?;
let path = path.ok_or_else(|| anyhow::anyhow!("Missing path"))?;
Ok(Self {
mode,
path,
recursive,
preserve_times,
user_info,
root_dir: config.root.clone().unwrap_or_else(|| PathBuf::from("/")),
audit: None,
filter: None,
})
}
/// Run SCP protocol
pub async fn run<R, W>(&self, mut reader: R, mut writer: W) -> Result<()>
where
R: AsyncReadExt + Unpin,
W: AsyncWriteExt + Unpin,
{
match self.mode {
ScpMode::Sink => self.run_sink(&mut reader, &mut writer).await,
ScpMode::Source => self.run_source(&mut reader, &mut writer).await,
}
}
/// Sink mode: Receive files from client
async fn run_sink<R, W>(&self, reader: &mut R, writer: &mut W) -> Result<()>
where
R: AsyncReadExt + Unpin,
W: AsyncWriteExt + Unpin,
{
// Send ready signal
writer.write_all(&[0]).await?;
writer.flush().await?;
let mut current_dir = self.resolve_path(&self.path)?;
loop {
// Read command byte
let mut cmd = [0u8; 1];
if reader.read_exact(&mut cmd).await.is_err() {
break;
}
match cmd[0] {
b'C' => {
// File: C<mode> <size> <filename>
self.receive_file(reader, writer, ¤t_dir).await?;
}
b'D' => {
// Directory: D<mode> 0 <dirname>
if self.recursive {
current_dir = self.enter_directory(reader, writer, ¤t_dir).await?;
} else {
self.send_error(writer, "Recursive mode not enabled").await?;
}
}
b'E' => {
// End of directory
if let Some(parent) = current_dir.parent() {
current_dir = parent.to_path_buf();
}
writer.write_all(&[0]).await?;
}
b'T' => {
// Preserve times: T<mtime> 0 <atime> 0
self.read_times(reader).await?;
writer.write_all(&[0]).await?;
}
b'\x01' | b'\x02' => {
// Error from client
let msg = self.read_line(reader).await?;
tracing::warn!("SCP client error: {}", msg);
break;
}
_ => {
self.send_error(writer, "Unknown command").await?;
}
}
}
Ok(())
}
/// Receive a single file
async fn receive_file<R, W>(
&self,
reader: &mut R,
writer: &mut W,
target_dir: &Path,
) -> Result<()>
where
R: AsyncReadExt + Unpin,
W: AsyncWriteExt + Unpin,
{
// Read header: C<mode> <size> <filename>\n
let header = self.read_line(reader).await?;
let parts: Vec<&str> = header.splitn(3, ' ').collect();
if parts.len() != 3 {
self.send_error(writer, "Invalid file header").await?;
return Ok(());
}
let mode = u32::from_str_radix(parts[0].trim_start_matches('C'), 8)?;
let size: u64 = parts[1].parse()?;
let filename = parts[2].trim();
let target_path = target_dir.join(filename);
// Check filter
if let Some(ref filter) = self.filter {
if let FilterResult::Deny = filter.check(&target_path, Operation::Upload, &self.user_info.username) {
self.send_error(writer, "Upload denied by filter").await?;
// Consume file data
let mut discard = vec![0u8; size as usize];
reader.read_exact(&mut discard).await?;
return Ok(());
}
}
// Send ready
writer.write_all(&[0]).await?;
writer.flush().await?;
// Receive file data
let mut file = tokio::fs::File::create(&target_path).await?;
let mut remaining = size;
let mut buffer = vec![0u8; 64 * 1024];
while remaining > 0 {
let to_read = remaining.min(buffer.len() as u64) as usize;
let n = reader.read(&mut buffer[..to_read]).await?;
if n == 0 {
anyhow::bail!("Unexpected EOF");
}
file.write_all(&buffer[..n]).await?;
remaining -= n as u64;
}
// Read trailing null byte
let mut null = [0u8; 1];
reader.read_exact(&mut null).await?;
// Set permissions
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&target_path, std::fs::Permissions::from_mode(mode))?;
}
// Audit log
if let Some(ref audit) = self.audit {
audit.export(AuditEvent {
event_type: EventType::FileUploaded,
user: self.user_info.username.clone(),
path: Some(target_path),
bytes: Some(size),
..Default::default()
}).await?;
}
// Send success
writer.write_all(&[0]).await?;
writer.flush().await?;
Ok(())
}
/// Source mode: Send files to client
async fn run_source<R, W>(&self, reader: &mut R, writer: &mut W) -> Result<()>
where
R: AsyncReadExt + Unpin,
W: AsyncWriteExt + Unpin,
{
// Wait for ready signal
let mut ready = [0u8; 1];
reader.read_exact(&mut ready).await?;
if ready[0] != 0 {
anyhow::bail!("Client not ready");
}
let source_path = self.resolve_path(&self.path)?;
if source_path.is_dir() {
if self.recursive {
self.send_directory(reader, writer, &source_path).await?;
} else {
self.send_error(writer, "Is a directory").await?;
}
} else {
self.send_file(reader, writer, &source_path).await?;
}
Ok(())
}
/// Send a single file
async fn send_file<R, W>(
&self,
reader: &mut R,
writer: &mut W,
path: &Path,
) -> Result<()>
where
R: AsyncReadExt + Unpin,
W: AsyncWriteExt + Unpin,
{
let metadata = tokio::fs::metadata(path).await?;
let size = metadata.len();
#[cfg(unix)]
let mode = {
use std::os::unix::fs::PermissionsExt;
metadata.permissions().mode() & 0o777
};
#[cfg(not(unix))]
let mode = 0o644;
let filename = path.file_name()
.map(|n| n.to_string_lossy())
.unwrap_or_default();
// Send header
let header = format!("C{:04o} {} {}\n", mode, size, filename);
writer.write_all(header.as_bytes()).await?;
writer.flush().await?;
// Wait for acknowledgment
let mut ack = [0u8; 1];
reader.read_exact(&mut ack).await?;
if ack[0] != 0 {
anyhow::bail!("Client rejected file");
}
// Send file data
let mut file = tokio::fs::File::open(path).await?;
let mut buffer = vec![0u8; 64 * 1024];
loop {
let n = file.read(&mut buffer).await?;
if n == 0 {
break;
}
writer.write_all(&buffer[..n]).await?;
}
// Send trailing null
writer.write_all(&[0]).await?;
writer.flush().await?;
// Wait for final acknowledgment
reader.read_exact(&mut ack).await?;
Ok(())
}
// Helper methods...
}2. Integrate with Command Execution
// Update src/server/exec.rs or handler.rs
impl CommandExecutor {
pub async fn execute(...) -> Result<i32> {
// Check if this is an SCP command
if command.starts_with("scp ") {
if let Ok(scp_handler) = ScpHandler::from_command(command, user_info, &self.config.scp) {
return self.run_scp(scp_handler, channel, session).await;
}
}
// Regular command execution...
}
}Files to Create/Modify
| File | Action |
|---|---|
src/server/scp.rs |
Create - SCP protocol handler |
src/server/exec.rs |
Modify - Integrate SCP detection |
src/server/mod.rs |
Modify - Add scp module |
Testing Requirements
- Unit test: SCP command parsing
- Integration test: File upload with scp
- Integration test: File download with scp
- Integration test: Recursive directory transfer
- Integration test: Permission preservation
# Test file upload
scp -P 2222 local.txt testuser@localhost:/tmp/
# Test file download
scp -P 2222 testuser@localhost:/tmp/remote.txt .
# Test recursive
scp -r -P 2222 ./mydir testuser@localhost:/tmp/Acceptance Criteria
- SCP command parsing (-t, -f, -r, -p flags)
- Sink mode (file upload)
- Source mode (file download)
- Recursive directory transfer
- Permission preservation
- Filter integration
- Audit logging
- Tests passing
Metadata
Metadata
Assignees
Labels
priority:mediumMedium priority issueMedium priority issuestatus:doneCompletedCompletedtype:enhancementNew feature or requestNew feature or request