use crate::transport::{Transport, Message, ChannelMessage, ChannelType};
use crate::subsystems::sftp::protocol::*;
use crate::{Result, QsshError};
pub struct SftpClient {
transport: Transport,
channel_id: u32,
next_id: u32,
}
impl SftpClient {
pub async fn connect(transport: Transport) -> Result<Self> {
let channel_id = rand::random::<u32>() % 65536;
let open_msg = Message::Channel(ChannelMessage::Open {
channel_id,
channel_type: ChannelType::Session,
window_size: 1024 * 1024,
max_packet_size: 32768,
});
transport.send_message(&open_msg).await?;
let start = std::time::Instant::now();
loop {
if start.elapsed() > std::time::Duration::from_secs(5) {
return Err(QsshError::Protocol("Timeout waiting for SFTP channel accept".into()));
}
match transport.receive_message::<Message>().await {
Ok(Message::Channel(ChannelMessage::Accept { channel_id: ch_id, .. })) if ch_id == channel_id => break,
Ok(_) => {}
Err(e) => return Err(e),
}
}
let subsystem_msg = Message::Channel(ChannelMessage::SubsystemRequest {
channel_id,
subsystem: "sftp".to_string(),
});
transport.send_message(&subsystem_msg).await?;
let start = std::time::Instant::now();
loop {
if start.elapsed() > std::time::Duration::from_secs(5) {
return Err(QsshError::Protocol("Timeout waiting for SFTP subsystem start".into()));
}
match transport.receive_message::<Message>().await {
Ok(Message::Channel(ChannelMessage::Data { channel_id: ch_id, data })) if ch_id == channel_id => {
if data == vec![0] {
break; }
let msg = String::from_utf8_lossy(&data);
return Err(QsshError::Protocol(format!("SFTP subsystem error: {}", msg)));
}
Ok(_) => {}
Err(e) => return Err(e),
}
}
let mut client = Self {
transport,
channel_id,
next_id: 1,
};
client.sftp_init().await?;
Ok(client)
}
fn next_request_id(&mut self) -> u32 {
let id = self.next_id;
self.next_id += 1;
id
}
async fn send_sftp(&self, msg: &SftpMessage) -> Result<()> {
let data = msg.to_bytes()
.map_err(|e| QsshError::Protocol(format!("SFTP serialize: {}", e)))?;
let channel_msg = Message::Channel(ChannelMessage::Data {
channel_id: self.channel_id,
data,
});
self.transport.send_message(&channel_msg).await
}
async fn recv_sftp(&self) -> Result<SftpMessage> {
loop {
match self.transport.receive_message::<Message>().await {
Ok(Message::Channel(ChannelMessage::Data { channel_id: ch_id, data })) if ch_id == self.channel_id => {
let msg = SftpMessage::from_bytes(&data)
.map_err(|e| QsshError::Protocol(format!("SFTP parse: {}", e)))?;
return Ok(msg);
}
Ok(Message::Channel(ChannelMessage::Eof { channel_id: ch_id })) if ch_id == self.channel_id => {
return Err(QsshError::Protocol("SFTP channel EOF".into()));
}
Ok(Message::Channel(ChannelMessage::Close { channel_id: ch_id })) if ch_id == self.channel_id => {
return Err(QsshError::Protocol("SFTP channel closed".into()));
}
Ok(_) => {} Err(e) => return Err(e),
}
}
}
async fn sftp_init(&mut self) -> Result<()> {
let init = SftpMessage::Init { version: 3 };
self.send_sftp(&init).await?;
match self.recv_sftp().await? {
SftpMessage::Version { version, .. } => {
log::info!("SFTP server version: {}", version);
Ok(())
}
other => Err(QsshError::Protocol(format!("Expected Version, got: {:?}", other))),
}
}
pub async fn upload(&mut self, local_data: &[u8], remote_path: &str) -> Result<()> {
let id = self.next_request_id();
let open_msg = SftpMessage::Open(OpenMessage {
id,
filename: remote_path.to_string(),
flags: OpenFlags {
read: false,
write: true,
append: false,
create: true,
truncate: true,
exclusive: false,
},
attrs: FileAttributes::default(),
});
self.send_sftp(&open_msg).await?;
let handle = match self.recv_sftp().await? {
SftpMessage::Handle { handle, .. } => handle,
SftpMessage::Status { code, message, .. } => {
return Err(QsshError::Protocol(format!("SFTP open failed: {:?} - {}", code, message)));
}
other => return Err(QsshError::Protocol(format!("Expected Handle, got: {:?}", other))),
};
let chunk_size = 32768; let mut offset = 0u64;
for chunk in local_data.chunks(chunk_size) {
let id = self.next_request_id();
let write_msg = SftpMessage::Write(WriteMessage {
id,
handle: handle.clone(),
offset,
data: chunk.to_vec(),
});
self.send_sftp(&write_msg).await?;
match self.recv_sftp().await? {
SftpMessage::Status { code: StatusCode::Ok, .. } => {}
SftpMessage::Status { code, message, .. } => {
return Err(QsshError::Protocol(format!("SFTP write failed: {:?} - {}", code, message)));
}
other => return Err(QsshError::Protocol(format!("Expected Status, got: {:?}", other))),
}
offset += chunk.len() as u64;
}
let id = self.next_request_id();
let close_msg = SftpMessage::Close { id, handle };
self.send_sftp(&close_msg).await?;
match self.recv_sftp().await? {
SftpMessage::Status { code: StatusCode::Ok, .. } => Ok(()),
SftpMessage::Status { code, message, .. } => {
Err(QsshError::Protocol(format!("SFTP close failed: {:?} - {}", code, message)))
}
other => Err(QsshError::Protocol(format!("Expected Status, got: {:?}", other))),
}
}
pub async fn download(&mut self, remote_path: &str) -> Result<Vec<u8>> {
let id = self.next_request_id();
let open_msg = SftpMessage::Open(OpenMessage {
id,
filename: remote_path.to_string(),
flags: OpenFlags {
read: true,
write: false,
append: false,
create: false,
truncate: false,
exclusive: false,
},
attrs: FileAttributes::default(),
});
self.send_sftp(&open_msg).await?;
let handle = match self.recv_sftp().await? {
SftpMessage::Handle { handle, .. } => handle,
SftpMessage::Status { code, message, .. } => {
return Err(QsshError::Protocol(format!("SFTP open failed: {:?} - {}", code, message)));
}
other => return Err(QsshError::Protocol(format!("Expected Handle, got: {:?}", other))),
};
let mut data = Vec::new();
let chunk_size = 32768u32;
let mut offset = 0u64;
loop {
let id = self.next_request_id();
let read_msg = SftpMessage::Read(ReadMessage {
id,
handle: handle.clone(),
offset,
length: chunk_size,
});
self.send_sftp(&read_msg).await?;
match self.recv_sftp().await? {
SftpMessage::Data { data: chunk, .. } => {
offset += chunk.len() as u64;
data.extend(chunk);
}
SftpMessage::Status { code: StatusCode::Eof, .. } => break,
SftpMessage::Status { code, message, .. } => {
return Err(QsshError::Protocol(format!("SFTP read failed: {:?} - {}", code, message)));
}
other => return Err(QsshError::Protocol(format!("Expected Data/Eof, got: {:?}", other))),
}
}
let id = self.next_request_id();
let close_msg = SftpMessage::Close { id, handle };
self.send_sftp(&close_msg).await?;
let _ = self.recv_sftp().await?;
Ok(data)
}
pub async fn mkdir(&mut self, path: &str) -> Result<()> {
let id = self.next_request_id();
let stat_msg = SftpMessage::Stat(StatMessage {
id,
path: path.to_string(),
});
self.send_sftp(&stat_msg).await?;
match self.recv_sftp().await? {
SftpMessage::Attrs { .. } => {
Ok(())
}
SftpMessage::Status { code: StatusCode::NoSuchFile, .. } => {
Err(QsshError::Protocol("Remote directory does not exist".into()))
}
other => Err(QsshError::Protocol(format!("Unexpected stat response: {:?}", other))),
}
}
pub async fn close(self) -> Result<()> {
let close_msg = Message::Channel(ChannelMessage::Close {
channel_id: self.channel_id,
});
let _ = self.transport.send_message(&close_msg).await;
Ok(())
}
}