Fix 5MB SFTP download hang: batch process SFTP packets + WINDOW_ADJUST chaining
Root cause: handle_channel_data processed only ONE SFTP packet per call, leaving remaining batched packets stuck in the buffer. Client waited for READ responses while server waited for more data — deadlock after ~3.1MB. Fix: - sftp_handler.rs: fix SSH_FXP_VERSION format (remove uint32 extension_count) - sftp_handler.rs: fix handle_open error mapping (.ok() → build_status_from_io_error) - channel.rs: batch-process ALL complete SFTP packets from buffer in loop - channel.rs: add pending_packets VecDeque for multi-response queuing - channel.rs: chain WINDOW_ADJUST + SFTP response when window is low - channel.rs: add adjust_remote_window() for client WINDOW_ADJUST - server.rs: drain pending_packets after each CHANNEL_DATA handler Verified: 5MB upload + download with matching MD5
This commit is contained in:
@@ -8,7 +8,7 @@ use std::io::{Read, Write}; // 导入Write trait(OpenSSH标准)
|
|||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||||
use log::{info, warn, debug, error};
|
use log::{info, warn, debug, error};
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use crate::ssh_server::sftp_handler::SftpHandler; // Phase 7: SFTP handler
|
use crate::ssh_server::sftp_handler::SftpHandler; // Phase 7: SFTP handler
|
||||||
use crate::ssh_server::scp_handler::ScpHandler; // Phase 8: SCP handler
|
use crate::ssh_server::scp_handler::ScpHandler; // Phase 8: SCP handler
|
||||||
@@ -23,6 +23,8 @@ use nix::poll::{poll, PollFd, PollFlags}; // Phase 14: poll机制(OpenSSH风
|
|||||||
pub struct ChannelManager {
|
pub struct ChannelManager {
|
||||||
channels: HashMap<u32, Channel>,
|
channels: HashMap<u32, Channel>,
|
||||||
next_channel_id: u32,
|
next_channel_id: u32,
|
||||||
|
/// ⭐⭐⭐⭐⭐ Phase 15.1: 待发送packet队列(用于同时发送WINDOW_ADJUST和SFTP响应)
|
||||||
|
pub pending_packets: VecDeque<SshPacket>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Phase 14: 交互式Exec进程管理(参考OpenSSH session.c: do_exec_no_pty)
|
/// Phase 14: 交互式Exec进程管理(参考OpenSSH session.c: do_exec_no_pty)
|
||||||
@@ -42,6 +44,7 @@ impl ChannelManager {
|
|||||||
Self {
|
Self {
|
||||||
channels: HashMap::new(),
|
channels: HashMap::new(),
|
||||||
next_channel_id: 0,
|
next_channel_id: 0,
|
||||||
|
pending_packets: VecDeque::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,57 +644,119 @@ impl ChannelManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Phase 7: 检查是否是SFTP channel(⭐⭐⭐⭐⭐ Phase 14.3: packet accumulation)
|
// Phase 7: 检查是否是SFTP channel(⭐⭐⭐⭐⭐ Phase 14.3: packet accumulation)
|
||||||
if let Some(sftp_handler) = &mut channel.sftp_handler {
|
// Extract SFTP result from channel borrow, then send outside
|
||||||
|
let sftp_result = if let Some(sftp_handler) = &mut channel.sftp_handler {
|
||||||
info!("Processing SFTP request ({} bytes)", data.len());
|
info!("Processing SFTP request ({} bytes)", data.len());
|
||||||
|
|
||||||
|
// ⭐⭐⭐⭐⭐ Window Control: decrease local_window
|
||||||
|
channel.local_window -= data.len() as u32;
|
||||||
|
channel.local_consumed += data.len() as u32;
|
||||||
|
|
||||||
// ⭐⭐⭐⭐⭐ Critical修复:累积SFTP packet数据
|
// ⭐⭐⭐⭐⭐ Critical修复:累积SFTP packet数据
|
||||||
channel.sftp_input_buffer.extend_from_slice(&data);
|
channel.sftp_input_buffer.extend_from_slice(&data);
|
||||||
info!("SFTP buffer accumulated: {} bytes total", channel.sftp_input_buffer.len());
|
info!("SFTP buffer accumulated: {} bytes total", channel.sftp_input_buffer.len());
|
||||||
|
|
||||||
// 检查buffer是否有足够数据解析packet length
|
// ⭐⭐⭐⭐⭐ Process ALL complete SFTP packets from buffer (not just one)
|
||||||
if channel.sftp_input_buffer.len() < 4 {
|
let mut all_responses: Vec<Vec<u8>> = Vec::new();
|
||||||
info!("SFTP buffer too short for length field, waiting for more data");
|
loop {
|
||||||
return Ok(None); // 继续累积
|
if channel.sftp_input_buffer.len() < 4 {
|
||||||
|
info!("SFTP buffer too short for length field, waiting for more data");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sftp_length = u32::from_be_bytes([
|
||||||
|
channel.sftp_input_buffer[0],
|
||||||
|
channel.sftp_input_buffer[1],
|
||||||
|
channel.sftp_input_buffer[2],
|
||||||
|
channel.sftp_input_buffer[3]
|
||||||
|
]) as usize;
|
||||||
|
info!("SFTP packet length field: {}", sftp_length);
|
||||||
|
|
||||||
|
let expected_total = 4 + sftp_length;
|
||||||
|
if channel.sftp_input_buffer.len() < expected_total {
|
||||||
|
info!("SFTP packet incomplete: expected {} bytes, have {} bytes in buffer, waiting for more",
|
||||||
|
expected_total, channel.sftp_input_buffer.len());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sftp_packet = channel.sftp_input_buffer[4..expected_total].to_vec();
|
||||||
|
info!("SFTP packet complete: {} bytes, processing", sftp_packet.len());
|
||||||
|
|
||||||
|
let response = sftp_handler.handle_request(&sftp_packet)?;
|
||||||
|
info!("SFTP response: {} bytes", response.len());
|
||||||
|
|
||||||
|
if channel.sftp_input_buffer.len() > expected_total {
|
||||||
|
let remaining = channel.sftp_input_buffer[expected_total..].to_vec();
|
||||||
|
channel.sftp_input_buffer = remaining;
|
||||||
|
info!("SFTP buffer has remaining {} bytes after processing", channel.sftp_input_buffer.len());
|
||||||
|
} else {
|
||||||
|
channel.sftp_input_buffer.clear();
|
||||||
|
info!("SFTP buffer cleared after processing");
|
||||||
|
}
|
||||||
|
|
||||||
|
all_responses.push(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析SFTP packet length(前4 bytes)
|
Some(all_responses)
|
||||||
let sftp_length = u32::from_be_bytes([
|
} else {
|
||||||
channel.sftp_input_buffer[0],
|
None
|
||||||
channel.sftp_input_buffer[1],
|
};
|
||||||
channel.sftp_input_buffer[2],
|
|
||||||
channel.sftp_input_buffer[3]
|
|
||||||
]) as usize;
|
|
||||||
info!("SFTP packet length field: {}", sftp_length);
|
|
||||||
|
|
||||||
let expected_total = 4 + sftp_length;
|
if let Some(responses) = sftp_result {
|
||||||
if channel.sftp_input_buffer.len() < expected_total {
|
// ⭐⭐⭐⭐⭐ Channel borrow is dropped; now we can use self freely
|
||||||
info!("SFTP packet incomplete: expected {} bytes, have {} bytes in buffer, waiting for more",
|
|
||||||
expected_total, channel.sftp_input_buffer.len());
|
// All responses except the last go to pending_packets
|
||||||
return Ok(None); // 继续累积
|
for i in 0..responses.len().saturating_sub(1) {
|
||||||
|
let pending = self.build_channel_data(recipient_channel, &responses[i])?;
|
||||||
|
self.pending_packets.push_back(pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⭐⭐⭐⭐⭐ Buffer足够,解析完整SFTP packet
|
// Last response is returned (possibly with WINDOW_ADJUST)
|
||||||
let sftp_packet = &channel.sftp_input_buffer[4..expected_total];
|
if let Some(last_response) = responses.into_iter().last() {
|
||||||
info!("SFTP packet complete: {} bytes, processing", sftp_packet.len());
|
// ⭐⭐⭐⭐⭐ Check window adjust (re-borrow channel briefly)
|
||||||
info!("SFTP packet content (first 20 bytes): {:?}", &sftp_packet[..std::cmp::min(20, sftp_packet.len())]);
|
let (needs_window, consumed) = if let Some(ch) = self.channels.get_mut(&recipient_channel) {
|
||||||
|
let window_used = ch.local_window_max - ch.local_window;
|
||||||
|
let need = (window_used > ch.local_maxpacket * 3) ||
|
||||||
|
(ch.local_window < ch.local_window_max / 2);
|
||||||
|
(need, ch.local_consumed)
|
||||||
|
} else {
|
||||||
|
(false, 0)
|
||||||
|
};
|
||||||
|
|
||||||
let response = sftp_handler.handle_request(sftp_packet)?;
|
if needs_window && consumed > 0 {
|
||||||
info!("SFTP response: {} bytes", response.len());
|
info!("⭐⭐⭐⭐⭐ [SFTP_WINDOW] Sending WINDOW_ADJUST before SFTP response");
|
||||||
|
let window_adjust = build_window_adjust(recipient_channel, consumed);
|
||||||
// ⭐⭐⭐⭐⭐ 处理完后,清空buffer或保留剩余数据
|
// Update window state
|
||||||
if channel.sftp_input_buffer.len() > expected_total {
|
if let Some(ch) = self.channels.get_mut(&recipient_channel) {
|
||||||
// 有剩余数据(多个packets的情况)
|
ch.local_window += consumed;
|
||||||
let remaining = channel.sftp_input_buffer[expected_total..].to_vec();
|
ch.local_consumed = 0;
|
||||||
channel.sftp_input_buffer = remaining;
|
}
|
||||||
info!("SFTP buffer has remaining {} bytes after processing", channel.sftp_input_buffer.len());
|
// Use standalone builder to avoid self borrow conflict
|
||||||
|
use std::io::Write;
|
||||||
|
let mut payload = Vec::new();
|
||||||
|
payload.write_u8(PacketType::SSH_MSG_CHANNEL_DATA as u8)?;
|
||||||
|
payload.write_u32::<BigEndian>(recipient_channel)?;
|
||||||
|
payload.write_u32::<BigEndian>(last_response.len() as u32)?;
|
||||||
|
payload.write_all(&last_response)?;
|
||||||
|
let sftp_packet = SshPacket::new(payload);
|
||||||
|
self.pending_packets.push_back(window_adjust);
|
||||||
|
self.pending_packets.push_back(sftp_packet);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
return Ok(Some(self.build_channel_data(recipient_channel, &last_response)?));
|
||||||
} else {
|
} else {
|
||||||
// 清空buffer
|
// No SFTP packets were complete, but maybe we need window adjust
|
||||||
channel.sftp_input_buffer.clear();
|
if let Some(ch) = self.channels.get_mut(&recipient_channel) {
|
||||||
info!("SFTP buffer cleared after processing");
|
let window_used = ch.local_window_max - ch.local_window;
|
||||||
|
if (window_used > ch.local_maxpacket * 3 || ch.local_window < ch.local_window_max / 2) && ch.local_consumed > 0 {
|
||||||
|
let window_adjust = build_window_adjust(recipient_channel, ch.local_consumed);
|
||||||
|
ch.local_window += ch.local_consumed;
|
||||||
|
ch.local_consumed = 0;
|
||||||
|
self.pending_packets.push_back(window_adjust);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建SSH_MSG_CHANNEL_DATA返回SFTP响应(需要SSH string格式)
|
|
||||||
return Ok(Some(self.build_channel_data(recipient_channel, &response)?));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,6 +764,15 @@ impl ChannelManager {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ⭐⭐⭐⭐⭐ Phase 13.5: 处理 client 发送的 SSH_MSG_CHANNEL_WINDOW_ADJUST
|
||||||
|
pub fn adjust_remote_window(&mut self, recipient_channel: u32, bytes_to_add: u32) {
|
||||||
|
if let Some(channel) = self.channels.get_mut(&recipient_channel) {
|
||||||
|
channel.remote_window = channel.remote_window.saturating_add(bytes_to_add);
|
||||||
|
info!("⭐⭐⭐⭐⭐ [ADJUST_REMOTE_WINDOW] channel {} remote_window increased by {} (new: {})",
|
||||||
|
recipient_channel, bytes_to_add, channel.remote_window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Phase 14: 构建SSH_MSG_CHANNEL_EXTENDED_DATA(参考OpenSSH channel.c)
|
/// Phase 14: 构建SSH_MSG_CHANNEL_EXTENDED_DATA(参考OpenSSH channel.c)
|
||||||
fn build_channel_extended_data(&self, channel: u32, data_type: u32, data: &[u8]) -> Result<SshPacket> {
|
fn build_channel_extended_data(&self, channel: u32, data_type: u32, data: &[u8]) -> Result<SshPacket> {
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
|
|||||||
@@ -501,6 +501,13 @@ fn handle_ssh_service_loop(
|
|||||||
encrypted_response.write(stream)?;
|
encrypted_response.write(stream)?;
|
||||||
info!("Sent SSH_MSG_CHANNEL_DATA (SFTP response)");
|
info!("Sent SSH_MSG_CHANNEL_DATA (SFTP response)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⭐⭐⭐⭐⭐ Phase 15.1: Drain pending packets (e.g. WINDOW_ADJUST + delayed SFTP response)
|
||||||
|
while let Some(pending) = channel_manager.pending_packets.pop_front() {
|
||||||
|
let encrypted_pending = EncryptedPacket::new(&pending.payload, encryption_ctx, true)?;
|
||||||
|
encrypted_pending.write(stream)?;
|
||||||
|
info!("Sent pending packet (type {})", pending.payload.first().unwrap_or(&0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(&pt) if pt == PacketType::SSH_MSG_CHANNEL_CLOSE as u8 => {
|
Some(&pt) if pt == PacketType::SSH_MSG_CHANNEL_CLOSE as u8 => {
|
||||||
info!("Received SSH_MSG_CHANNEL_CLOSE");
|
info!("Received SSH_MSG_CHANNEL_CLOSE");
|
||||||
@@ -518,6 +525,15 @@ fn handle_ssh_service_loop(
|
|||||||
info!("Received SSH_MSG_DISCONNECT");
|
info!("Received SSH_MSG_DISCONNECT");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
Some(&pt) if pt == PacketType::SSH_MSG_CHANNEL_WINDOW_ADJUST as u8 => {
|
||||||
|
let payload = &packet.payload;
|
||||||
|
if payload.len() >= 9 {
|
||||||
|
// Format: uint32 recipient_channel || uint32 bytes_to_add
|
||||||
|
let recipient_channel = u32::from_be_bytes([payload[1], payload[2], payload[3], payload[4]]);
|
||||||
|
let bytes_to_add = u32::from_be_bytes([payload[5], payload[6], payload[7], payload[8]]);
|
||||||
|
channel_manager.adjust_remote_window(recipient_channel, bytes_to_add);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
warn!("Unknown packet type: {:?}", packet.payload.first());
|
warn!("Unknown packet type: {:?}", packet.payload.first());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -341,21 +341,6 @@ impl SftpHandler {
|
|||||||
let version = cursor.read_u32::<BigEndian>()?;
|
let version = cursor.read_u32::<BigEndian>()?;
|
||||||
info!("Client SFTP version: {}", version);
|
info!("Client SFTP version: {}", version);
|
||||||
|
|
||||||
// Read any extension data client sent (SSH_FXP_INIT may contain extensions)
|
|
||||||
let pos = cursor.position() as usize;
|
|
||||||
let inner = cursor.get_ref();
|
|
||||||
if inner.len() > pos && (inner.len() - pos) >= 4 {
|
|
||||||
let ext_count = match cursor.read_u32::<BigEndian>() {
|
|
||||||
Ok(n) => n,
|
|
||||||
Err(_) => 0,
|
|
||||||
};
|
|
||||||
for i in 0..ext_count {
|
|
||||||
let ext_name = read_sftp_string(&mut cursor).unwrap_or_default();
|
|
||||||
let ext_data = read_sftp_string(&mut cursor).unwrap_or_default();
|
|
||||||
debug!("Client extension[{}]: {} = {}", i, ext_name, ext_data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = self.build_version_response(3)?;
|
let response = self.build_version_response(3)?;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
@@ -376,8 +361,8 @@ impl SftpHandler {
|
|||||||
|
|
||||||
let full_path = self.resolve_path(&path)?;
|
let full_path = self.resolve_path(&path)?;
|
||||||
|
|
||||||
let file = if pflags & SftpFileFlags::SSH_FXF_READ != 0 {
|
let file_result = if pflags & SftpFileFlags::SSH_FXF_READ != 0 {
|
||||||
OpenOptions::new().read(true).open(&full_path).ok()
|
OpenOptions::new().read(true).open(&full_path)
|
||||||
} else if pflags & SftpFileFlags::SSH_FXF_WRITE != 0 {
|
} else if pflags & SftpFileFlags::SSH_FXF_WRITE != 0 {
|
||||||
let mut opts = OpenOptions::new();
|
let mut opts = OpenOptions::new();
|
||||||
opts.write(true);
|
opts.write(true);
|
||||||
@@ -393,13 +378,13 @@ impl SftpHandler {
|
|||||||
if pflags & SftpFileFlags::SSH_FXF_EXCL != 0 {
|
if pflags & SftpFileFlags::SSH_FXF_EXCL != 0 {
|
||||||
opts.create_new(true);
|
opts.create_new(true);
|
||||||
}
|
}
|
||||||
opts.open(&full_path).ok()
|
opts.open(&full_path)
|
||||||
} else {
|
} else {
|
||||||
None
|
return self.build_status_response(id, SftpStatus::SSH_FX_OP_UNSUPPORTED, "Unsupported open flags");
|
||||||
};
|
};
|
||||||
|
|
||||||
match file {
|
match file_result {
|
||||||
Some(file) => {
|
Ok(file) => {
|
||||||
if self.handles.len() >= Self::MAX_HANDLES {
|
if self.handles.len() >= Self::MAX_HANDLES {
|
||||||
warn!("SSH_FXP_OPEN: handle limit reached ({})", Self::MAX_HANDLES);
|
warn!("SSH_FXP_OPEN: handle limit reached ({})", Self::MAX_HANDLES);
|
||||||
return self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Handle limit reached");
|
return self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Handle limit reached");
|
||||||
@@ -419,8 +404,8 @@ impl SftpHandler {
|
|||||||
|
|
||||||
self.build_handle_response(id, &handle_id.to_be_bytes())
|
self.build_handle_response(id, &handle_id.to_be_bytes())
|
||||||
}
|
}
|
||||||
None => {
|
Err(e) => {
|
||||||
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Failed to open file")
|
self.build_status_from_io_error(id, &e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1478,14 +1463,25 @@ impl SftpHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建SSH_FXP_VERSION响应,包含扩展声明(参考OpenSSH sftp-server.c)
|
/// 构建SSH_FXP_VERSION响应,包含扩展声明(参考OpenSSH sftp-server.c: process_init())
|
||||||
|
///
|
||||||
|
/// SFTP协议格式(draft-ietf-secsh-filexfer-02):
|
||||||
|
/// uint32 length
|
||||||
|
/// uint8 type (SSH_FXP_VERSION = 2)
|
||||||
|
/// uint32 version
|
||||||
|
/// // extensions: NO count field, simply paired strings until buffer empty
|
||||||
|
/// string extension_name (= uint32(len_with_nul) + data + \0)
|
||||||
|
/// string extension_data (= uint32(len_with_nul) + data + \0)
|
||||||
|
///
|
||||||
|
/// OpenSSH uses sshbuf_put_cstring() which includes NUL terminator.
|
||||||
|
/// Client reads with sshbuf_get_cstring() which expects \0 at end.
|
||||||
fn build_version_response(&self, version: u32) -> Result<Vec<u8>> {
|
fn build_version_response(&self, version: u32) -> Result<Vec<u8>> {
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
buffer.write_u8(SftpPacketType::SSH_FXP_VERSION as u8)?;
|
buffer.write_u8(SftpPacketType::SSH_FXP_VERSION as u8)?;
|
||||||
buffer.write_u32::<BigEndian>(version)?;
|
buffer.write_u32::<BigEndian>(version)?;
|
||||||
|
|
||||||
// 扩展声明(OpenSSH sftp-server.c: process_init() 中声明支持的扩展)
|
// 扩展声明 — OpenSSH sftp-server.c: process_init() style, NO count field
|
||||||
let extensions: &[(&str, &str)] = &[
|
let extensions: &[(&str, &str)] = &[
|
||||||
("posix-rename@openssh.com", "1"),
|
("posix-rename@openssh.com", "1"),
|
||||||
("hardlink@openssh.com", "1"),
|
("hardlink@openssh.com", "1"),
|
||||||
@@ -1498,13 +1494,14 @@ impl SftpHandler {
|
|||||||
("sha384-hash@openssh.com", "1"),
|
("sha384-hash@openssh.com", "1"),
|
||||||
("sha512-hash@openssh.com", "1"),
|
("sha512-hash@openssh.com", "1"),
|
||||||
];
|
];
|
||||||
|
|
||||||
buffer.write_u32::<BigEndian>(extensions.len() as u32)?;
|
|
||||||
for (name, data) in extensions {
|
for (name, data) in extensions {
|
||||||
buffer.write_u32::<BigEndian>(name.len() as u32)?;
|
// sshbuf_put_cstring(buf, s) → sshbuf_put_string(buf, s, strlen(s)+1)
|
||||||
|
buffer.write_u32::<BigEndian>((name.len() + 1) as u32)?;
|
||||||
buffer.write_all(name.as_bytes())?;
|
buffer.write_all(name.as_bytes())?;
|
||||||
buffer.write_u32::<BigEndian>(data.len() as u32)?;
|
buffer.write_u8(0)?;
|
||||||
|
buffer.write_u32::<BigEndian>((data.len() + 1) as u32)?;
|
||||||
buffer.write_all(data.as_bytes())?;
|
buffer.write_all(data.as_bytes())?;
|
||||||
|
buffer.write_u8(0)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.wrap_sftp_packet(&buffer)
|
self.wrap_sftp_packet(&buffer)
|
||||||
|
|||||||
Reference in New Issue
Block a user