1use std::io::{Read, Seek, Write};
2use std::path::{Path, PathBuf};
3use std::str::FromStr as _;
4use std::time::UNIX_EPOCH;
5
6use libssh_rs::{AuthMethods, AuthStatus, OpenFlags, SshKey, SshOption};
7use remotefs::fs::stream::{ReadAndSeek, WriteAndSeek};
8use remotefs::fs::{FileType, Metadata, ReadStream, UnixPex, WriteStream};
9use remotefs::{File, RemoteError, RemoteErrorType, RemoteResult};
10
11use super::SshSession;
12use crate::SshOpts;
13use crate::ssh::backend::Sftp;
14use crate::ssh::config::Config;
15
16pub struct LibSshSession {
20 session: libssh_rs::Session,
21}
22
23pub struct LibSshSftp {
27 inner: libssh_rs::Sftp,
28}
29
30struct ScpRecvChannel {
32 channel: libssh_rs::Channel,
33 filesize: usize,
36 read: usize,
37}
38
39impl Read for ScpRecvChannel {
40 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
41 if self.read >= self.filesize {
42 return Ok(0);
43 }
44
45 let max_read = (self.filesize - self.read).min(buf.len());
47 let res = self.channel.stdout().read(&mut buf[..max_read])?;
48
49 self.read += res;
50 Ok(res)
51 }
52}
53
54impl Drop for ScpRecvChannel {
55 fn drop(&mut self) {
56 debug!("Dropping SCP recv channel");
57 if let Err(err) = self.channel.send_eof() {
58 debug!("Error sending EOF: {err}");
59 }
60 if let Err(err) = self.channel.close() {
61 debug!("Error closing channel: {err}");
62 }
63 }
64}
65
66struct ScpSendChannel {
68 channel: libssh_rs::Channel,
69}
70
71impl Write for ScpSendChannel {
72 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
73 self.channel.stdin().write(buf)
74 }
75
76 fn flush(&mut self) -> std::io::Result<()> {
77 self.channel.stdin().flush()
78 }
79}
80
81impl Drop for ScpSendChannel {
82 fn drop(&mut self) {
83 debug!("Dropping SCP send channel");
84 if let Err(err) = self.channel.send_eof() {
85 debug!("Error sending EOF: {err}");
86 }
87 if let Err(err) = self.channel.close() {
88 debug!("Error closing channel: {err}");
89 }
90 }
91}
92
93impl SshSession for LibSshSession {
94 type Sftp = LibSshSftp;
95
96 fn connect(opts: &SshOpts) -> remotefs::RemoteResult<Self> {
97 debug!("Connecting to '{}'", opts.host);
99
100 let mut session = match libssh_rs::Session::new() {
102 Ok(s) => s,
103 Err(err) => {
104 error!("Could not create session: {err}");
105 return Err(RemoteError::new_ex(RemoteErrorType::ConnectionError, err));
106 }
107 };
108
109 session
111 .set_option(SshOption::Hostname(opts.host.clone()))
112 .map_err(|e| RemoteError::new_ex(RemoteErrorType::ConnectionError, e))?;
113 if let Some(port) = opts.port {
114 debug!("Using port: {port}");
115 session
116 .set_option(SshOption::Port(port))
117 .map_err(|e| RemoteError::new_ex(RemoteErrorType::ConnectionError, e))?;
118 }
119
120 let config_file_str = opts.config_file.as_ref().map(|p| p.display().to_string());
121 debug!("Using config file: {:?}", config_file_str);
122 session
123 .options_parse_config(config_file_str.as_deref())
124 .map_err(|e| RemoteError::new_ex(RemoteErrorType::ConnectionError, e))?;
125
126 for opt in opts.methods.iter().filter_map(|method| method.ssh_opts()) {
128 debug!("Setting SSH option: {opt:?}");
129 session
130 .set_option(opt)
131 .map_err(|e| RemoteError::new_ex(RemoteErrorType::ConnectionError, e))?;
132 }
133
134 if let Err(err) = session.connect() {
136 error!("SSH handshake failed: {err}");
137 return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
138 }
139
140 authenticate(&mut session, opts)?;
142
143 Ok(Self { session })
144 }
145
146 fn authenticated(&self) -> RemoteResult<bool> {
147 Ok(self.session.is_connected())
148 }
149
150 fn banner(&self) -> RemoteResult<Option<String>> {
151 self.session.get_server_banner().map(Some).map_err(|e| {
152 RemoteError::new_ex(
153 RemoteErrorType::ProtocolError,
154 format!("Failed to get banner: {e}"),
155 )
156 })
157 }
158
159 fn disconnect(&self) -> RemoteResult<()> {
160 self.session.disconnect();
161
162 Ok(())
163 }
164
165 fn cmd<S>(&mut self, cmd: S) -> RemoteResult<(u32, String)>
166 where
167 S: AsRef<str>,
168 {
169 let output = perform_shell_cmd(&mut self.session, format!("{}; echo $?", cmd.as_ref()))?;
170 if let Some(index) = output.trim().rfind('\n') {
171 trace!("Read from stdout: '{output}'");
172 let actual_output = (output[0..index + 1]).to_string();
173 trace!("Actual output '{actual_output}'");
174 trace!("Parsing return code '{}'", output[index..].trim());
175 let rc = match u32::from_str(output[index..].trim()).ok() {
176 Some(val) => val,
177 None => {
178 return Err(RemoteError::new_ex(
179 RemoteErrorType::ProtocolError,
180 "Failed to get command exit code",
181 ));
182 }
183 };
184 debug!(r#"Command output: "{actual_output}"; exit code: {rc}"#);
185 Ok((rc, actual_output))
186 } else {
187 match u32::from_str(output.trim()).ok() {
188 Some(val) => Ok((val, String::new())),
189 None => Err(RemoteError::new_ex(
190 RemoteErrorType::ProtocolError,
191 "Failed to get command exit code",
192 )),
193 }
194 }
195 }
196
197 fn scp_recv(&self, path: &Path) -> RemoteResult<Box<dyn Read + Send>> {
198 self.session.set_blocking(true);
199
200 debug!("Opening channel for scp recv");
202 let channel = self.session.new_channel().map_err(|err| {
203 RemoteError::new_ex(
204 RemoteErrorType::ProtocolError,
205 format!("Could not open channel: {err}"),
206 )
207 })?;
208 debug!("Opening channel session");
209 channel.open_session().map_err(|err| {
210 RemoteError::new_ex(
211 RemoteErrorType::ProtocolError,
212 format!("Could not open session: {err}"),
213 )
214 })?;
215 let cmd = format!("scp -f {}", path.display());
217 channel.request_exec(cmd.as_ref()).map_err(|err| {
218 RemoteError::new_ex(
219 RemoteErrorType::ProtocolError,
220 format!("Could not request command execution: {err}"),
221 )
222 })?;
223 debug!("ACK with 0");
224 channel.stdin().write_all(b"\0").map_err(|err| {
226 RemoteError::new_ex(
227 RemoteErrorType::ProtocolError,
228 format!("Could not write to channel: {err}"),
229 )
230 })?;
231
232 debug!("Reading SCP header");
234 let mut header = [0u8; 1024];
235 let bytes = channel.stdout().read(&mut header).map_err(|err| {
236 RemoteError::new_ex(
237 RemoteErrorType::ProtocolError,
238 format!("Could not read from channel: {err}"),
239 )
240 })?;
241 let filesize = parse_scp_header_filesize(&header[..bytes])?;
243 debug!("File size: {filesize}");
244 debug!("Sending OK");
246 channel.stdin().write_all(b"\0").map_err(|err| {
247 RemoteError::new_ex(
248 RemoteErrorType::ProtocolError,
249 format!("Could not write to channel: {err}"),
250 )
251 })?;
252
253 debug!("Creating SCP recv channel");
254 let reader = ScpRecvChannel {
255 channel,
256 filesize,
257 read: 0,
258 };
259
260 Ok(Box::new(reader) as Box<dyn Read + Send>)
261 }
262
263 fn scp_send(
264 &self,
265 remote_path: &Path,
266 mode: i32,
267 size: u64,
268 _times: Option<(u64, u64)>,
269 ) -> RemoteResult<Box<dyn Write + Send>> {
270 self.session.set_blocking(true);
271
272 debug!("Opening channel for scp send");
274 let channel = self.session.new_channel().map_err(|err| {
275 RemoteError::new_ex(
276 RemoteErrorType::ProtocolError,
277 format!("Could not open channel: {err}"),
278 )
279 })?;
280 debug!("Opening channel session");
281 channel.open_session().map_err(|err| {
282 RemoteError::new_ex(
283 RemoteErrorType::ProtocolError,
284 format!("Could not open session: {err}"),
285 )
286 })?;
287 let cmd = format!("scp -t {}", remote_path.display());
289 channel.request_exec(cmd.as_ref()).map_err(|err| {
290 RemoteError::new_ex(
291 RemoteErrorType::ProtocolError,
292 format!("Could not request command execution: {err}"),
293 )
294 })?;
295
296 wait_for_ack(&channel)?;
298
299 let Some(filename) = remote_path.file_name().map(|f| f.to_string_lossy()) else {
300 return Err(RemoteError::new_ex(
301 RemoteErrorType::ProtocolError,
302 format!("Could not get file name: {remote_path:?}"),
303 ));
304 };
305
306 let header = format!("C{mode:04o} {size} {filename}\n", mode = mode & 0o7777,);
308 debug!("Sending SCP header: {header}");
309 channel
310 .stdin()
311 .write_all(header.as_bytes())
312 .map_err(|err| {
313 RemoteError::new_ex(
314 RemoteErrorType::ProtocolError,
315 format!("Could not write to channel: {err}"),
316 )
317 })?;
318
319 wait_for_ack(&channel)?;
321
322 let writer = ScpSendChannel { channel };
324 Ok(Box::new(writer) as Box<dyn Write + Send>)
325 }
326
327 fn sftp(&self) -> RemoteResult<Self::Sftp> {
328 self.session
329 .sftp()
330 .map(|sftp| LibSshSftp { inner: sftp })
331 .map_err(|e| RemoteError::new_ex(RemoteErrorType::ProtocolError, e))
332 }
333}
334
335struct SftpFileReader(libssh_rs::SftpFile);
336
337struct SftpFileWriter(libssh_rs::SftpFile);
338
339impl Write for SftpFileWriter {
340 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
341 self.0.write(buf)
342 }
343
344 fn flush(&mut self) -> std::io::Result<()> {
345 self.0.flush()
346 }
347}
348
349impl Seek for SftpFileWriter {
350 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
351 self.0.seek(pos)
352 }
353}
354
355impl WriteAndSeek for SftpFileWriter {}
356
357impl Read for SftpFileReader {
358 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
359 self.0.read(buf)
360 }
361}
362
363impl Seek for SftpFileReader {
364 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
365 self.0.seek(pos)
366 }
367}
368
369impl ReadAndSeek for SftpFileReader {}
370
371impl Sftp for LibSshSftp {
372 fn mkdir(&self, path: &Path, mode: i32) -> RemoteResult<()> {
373 self.inner
374 .create_dir(conv_path_to_str(path), mode as u32)
375 .map_err(|err| {
376 RemoteError::new_ex(
377 RemoteErrorType::FileCreateDenied,
378 format!(
379 "Could not create directory '{path}': {err}",
380 path = path.display()
381 ),
382 )
383 })
384 }
385
386 fn open_read(&self, path: &Path) -> RemoteResult<ReadStream> {
387 self.inner
388 .open(conv_path_to_str(path), OpenFlags::READ_ONLY, 0)
389 .map(|file| ReadStream::from(Box::new(SftpFileReader(file)) as Box<dyn ReadAndSeek>))
390 .map_err(|err| {
391 RemoteError::new_ex(
392 RemoteErrorType::ProtocolError,
393 format!(
394 "Could not open file at '{path}': {err}",
395 path = path.display()
396 ),
397 )
398 })
399 }
400
401 fn open_write(
402 &self,
403 path: &Path,
404 flags: super::WriteMode,
405 mode: i32,
406 ) -> RemoteResult<WriteStream> {
407 let flags = match flags {
408 super::WriteMode::Append => {
409 OpenFlags::WRITE_ONLY | OpenFlags::APPEND | OpenFlags::CREATE
410 }
411 super::WriteMode::Truncate => {
412 OpenFlags::WRITE_ONLY | OpenFlags::CREATE | OpenFlags::TRUNCATE
413 }
414 };
415
416 self.inner
419 .open(conv_path_to_str(path), flags, mode as u32)
420 .map(|file| WriteStream::from(Box::new(SftpFileWriter(file)) as Box<dyn WriteAndSeek>))
421 .map_err(|err| {
422 RemoteError::new_ex(
423 RemoteErrorType::ProtocolError,
424 format!(
425 "Could not open file at '{path}': {err}",
426 path = path.display()
427 ),
428 )
429 })
430 }
431
432 fn readdir<T>(&self, dirname: T) -> RemoteResult<Vec<remotefs::File>>
433 where
434 T: AsRef<Path>,
435 {
436 self.inner
437 .read_dir(conv_path_to_str(dirname.as_ref()))
438 .map(|files| {
439 files
440 .into_iter()
441 .filter(|metadata| {
442 metadata.name() != Some(".") && metadata.name() != Some("..")
443 })
444 .map(|metadata| {
445 self.make_fsentry(MakePath::Directory(dirname.as_ref()), metadata)
446 })
447 .collect()
448 })
449 .map_err(|err| {
450 RemoteError::new_ex(
451 RemoteErrorType::ProtocolError,
452 format!("Could not read directory: {err}",),
453 )
454 })
455 }
456
457 fn realpath(&self, path: &Path) -> RemoteResult<PathBuf> {
458 self.inner
459 .read_link(conv_path_to_str(path))
460 .map(PathBuf::from)
461 .map_err(|err| {
462 RemoteError::new_ex(
463 RemoteErrorType::ProtocolError,
464 format!(
465 "Could not resolve real path for '{path}': {err}",
466 path = path.display()
467 ),
468 )
469 })
470 }
471
472 fn rename(&self, src: &Path, dest: &Path) -> RemoteResult<()> {
473 self.inner
474 .rename(conv_path_to_str(src), conv_path_to_str(dest))
475 .map_err(|err| {
476 RemoteError::new_ex(
477 RemoteErrorType::ProtocolError,
478 format!("Could not rename file '{src}': {err}", src = src.display()),
479 )
480 })
481 }
482
483 fn rmdir(&self, path: &Path) -> RemoteResult<()> {
484 self.inner
485 .remove_dir(conv_path_to_str(path))
486 .map_err(|err| {
487 RemoteError::new_ex(
488 RemoteErrorType::CouldNotRemoveFile,
489 format!(
490 "Could not remove directory '{path}': {err}",
491 path = path.display()
492 ),
493 )
494 })
495 }
496
497 fn setstat(&self, path: &Path, metadata: Metadata) -> RemoteResult<()> {
498 self.inner
499 .set_metadata(conv_path_to_str(path), &Self::set_attributes(metadata))
500 .map_err(|err| {
501 RemoteError::new_ex(
502 RemoteErrorType::ProtocolError,
503 format!(
504 "Could not set file attributes for '{path}': {err}",
505 path = path.display()
506 ),
507 )
508 })
509 }
510
511 fn stat(&self, filename: &Path) -> RemoteResult<File> {
512 self.inner
513 .metadata(conv_path_to_str(filename))
514 .map(|metadata| self.make_fsentry(MakePath::File(filename), metadata))
515 .map_err(|err| {
516 RemoteError::new_ex(
517 RemoteErrorType::ProtocolError,
518 format!(
519 "Could not get file attributes for '{filename}': {err}",
520 filename = filename.display()
521 ),
522 )
523 })
524 }
525
526 fn symlink(&self, path: &Path, target: &Path) -> RemoteResult<()> {
527 self.inner
528 .symlink(conv_path_to_str(path), conv_path_to_str(target))
529 .map_err(|err| {
530 RemoteError::new_ex(
531 RemoteErrorType::FileCreateDenied,
532 format!(
533 "Could not create symlink '{path}': {err}",
534 path = path.display()
535 ),
536 )
537 })
538 }
539
540 fn unlink(&self, path: &Path) -> RemoteResult<()> {
541 self.inner
542 .remove_file(conv_path_to_str(path))
543 .map_err(|err| {
544 RemoteError::new_ex(
545 RemoteErrorType::CouldNotRemoveFile,
546 format!(
547 "Could not remove file '{path}': {err}",
548 path = path.display()
549 ),
550 )
551 })
552 }
553}
554
555fn conv_path_to_str(path: &Path) -> &str {
556 path.to_str().unwrap_or_default()
557}
558
559enum MakePath<'a> {
560 Directory(&'a Path),
561 File(&'a Path),
562}
563
564impl LibSshSftp {
565 fn set_attributes(metadata: Metadata) -> libssh_rs::SetAttributes {
566 let atime = metadata.accessed.unwrap_or(UNIX_EPOCH);
567 let mtime = metadata.modified.unwrap_or(UNIX_EPOCH);
568
569 let uid_gid = match (metadata.uid, metadata.gid) {
570 (Some(uid), Some(gid)) => Some((uid, gid)),
571 _ => None,
572 };
573
574 libssh_rs::SetAttributes {
575 size: Some(metadata.size),
576 uid_gid,
577 permissions: metadata.mode.map(|m| m.into()),
578 atime_mtime: Some((atime, mtime)),
579 }
580 }
581
582 fn make_fsentry(&self, path: MakePath<'_>, metadata: libssh_rs::Metadata) -> File {
583 let name = match metadata.name() {
584 None => "/".to_string(),
585 Some(name) => name.to_string(),
586 };
587 debug!("Found file {name}");
588
589 let path = match path {
590 MakePath::Directory(dir) => dir.join(&name),
591 MakePath::File(file) => file.to_path_buf(),
592 };
593 debug!("Computed path for {name}: {path}", path = path.display());
594
595 let uid = metadata.uid();
597 let gid = metadata.gid();
598 let mode = metadata.permissions().map(UnixPex::from);
599 let size = metadata.len().unwrap_or(0);
600 let accessed = metadata.accessed();
601 let modified = metadata.modified();
602 let symlink = match metadata.file_type() {
603 Some(libssh_rs::FileType::Symlink) => match self.realpath(&path) {
604 Ok(p) => Some(p),
605 Err(err) => {
606 error!(
607 "Failed to read link of {} (even it's supposed to be a symlink): {err}",
608 path.display(),
609 );
610 None
611 }
612 },
613 _ => None,
614 };
615 let file_type = if symlink.is_some() {
616 FileType::Symlink
617 } else if matches!(metadata.file_type(), Some(libssh_rs::FileType::Directory)) {
618 FileType::Directory
619 } else {
620 FileType::File
621 };
622 let entry_metadata = Metadata {
623 accessed,
624 created: None,
625 file_type,
626 gid,
627 mode,
628 modified,
629 size,
630 symlink,
631 uid,
632 };
633 trace!("Metadata for {}: {:?}", path.display(), entry_metadata);
634 File {
635 path: path.to_path_buf(),
636 metadata: entry_metadata,
637 }
638 }
639}
640
641fn authenticate(session: &mut libssh_rs::Session, opts: &SshOpts) -> RemoteResult<()> {
642 let ssh_config = Config::try_from(opts)?;
644 let username = ssh_config.username.clone();
645
646 debug!("Authenticating to {}", opts.host);
647 session
648 .set_option(SshOption::User(Some(username)))
649 .map_err(|e| {
650 RemoteError::new_ex(
651 RemoteErrorType::AuthenticationFailed,
652 format!("Failed to set username: {e}"),
653 )
654 })?;
655
656 debug!("Trying with userauth_none");
657 match session.userauth_none(opts.username.as_deref()) {
658 Ok(AuthStatus::Success) => {
659 debug!("Authenticated with userauth_none");
660 return Ok(());
661 }
662 Ok(status) => {
663 debug!("userauth_none returned status: {status:?}");
664 }
665 Err(err) => {
666 debug!("userauth_none failed: {err}");
667 }
668 }
669
670 let auth_methods = session
671 .userauth_list(opts.username.as_deref())
672 .map_err(|e| RemoteError::new_ex(RemoteErrorType::AuthenticationFailed, e))?;
673 debug!("Available authentication methods: {auth_methods:?}");
674
675 if auth_methods.contains(AuthMethods::PUBLIC_KEY) {
676 debug!("Trying public key authentication");
677 match session.userauth_public_key_auto(None, None) {
679 Ok(AuthStatus::Success) => {
680 debug!("Authenticated with public key");
681 return Ok(());
682 }
683 Ok(status) => {
684 debug!("userauth_public_key_auto returned status: {status:?}");
685 }
686 Err(err) => {
687 debug!("userauth_public_key_auto failed: {err}");
688 }
689 }
690
691 match key_storage_auth(session, opts, &ssh_config) {
693 Ok(()) => {
694 debug!("Authenticated with public key from storage");
695 return Ok(());
696 }
697 Err(err) => {
698 debug!("Key storage authentication failed: {err}");
699 }
700 }
701 }
702
703 if auth_methods.contains(AuthMethods::PASSWORD) {
704 debug!("Trying password authentication");
705
706 match session.userauth_password(None, Some(opts.password.as_deref().unwrap_or_default())) {
708 Ok(AuthStatus::Success) => {
709 debug!("Authenticated with password");
710 return Ok(());
711 }
712 Ok(status) => {
713 debug!("userauth_password returned status: {status:?}");
714 }
715 Err(err) => {
716 debug!("userauth_password failed: {err}");
717 }
718 }
719 }
720
721 Err(RemoteError::new_ex(
722 RemoteErrorType::AuthenticationFailed,
723 "all authentication methods failed",
724 ))
725}
726
727fn key_storage_auth(
728 session: &mut libssh_rs::Session,
729 opts: &SshOpts,
730 ssh_config: &Config,
731) -> RemoteResult<()> {
732 let Some(key_storage) = &opts.key_storage else {
733 return Err(RemoteError::new_ex(
734 RemoteErrorType::AuthenticationFailed,
735 "no key storage available",
736 ));
737 };
738
739 let Some(priv_key_path) = key_storage
740 .resolve(&ssh_config.host, &ssh_config.username)
741 .or(key_storage.resolve(
742 ssh_config.resolved_host.as_str(),
743 ssh_config.username.as_str(),
744 ))
745 else {
746 return Err(RemoteError::new_ex(
747 RemoteErrorType::AuthenticationFailed,
748 "no key found in storage",
749 ));
750 };
751
752 let Ok(privkey) =
753 SshKey::from_privkey_file(conv_path_to_str(&priv_key_path), opts.password.as_deref())
754 else {
755 return Err(RemoteError::new_ex(
756 RemoteErrorType::AuthenticationFailed,
757 format!(
758 "could not load private key from file: {}",
759 priv_key_path.display()
760 ),
761 ));
762 };
763
764 match session
765 .userauth_publickey(opts.username.as_deref(), &privkey)
766 .map_err(|e| RemoteError::new_ex(RemoteErrorType::AuthenticationFailed, e))
767 {
768 Ok(AuthStatus::Success) => Ok(()),
769 Ok(status) => Err(RemoteError::new_ex(
770 RemoteErrorType::AuthenticationFailed,
771 format!("authentication failed: {status:?}"),
772 )),
773 Err(err) => Err(err),
774 }
775}
776
777fn perform_shell_cmd<S: AsRef<str>>(
778 session: &mut libssh_rs::Session,
779 cmd: S,
780) -> RemoteResult<String> {
781 trace!("Running command: {}", cmd.as_ref());
783 let channel = match session.new_channel() {
784 Ok(ch) => ch,
785 Err(err) => {
786 return Err(RemoteError::new_ex(
787 RemoteErrorType::ProtocolError,
788 format!("Could not open channel: {err}"),
789 ));
790 }
791 };
792
793 debug!("Opening channel session");
794 channel.open_session().map_err(|err| {
795 RemoteError::new_ex(
796 RemoteErrorType::ProtocolError,
797 format!("Could not open session: {err}"),
798 )
799 })?;
800
801 debug!("Requesting command execution: {}", cmd.as_ref());
802 channel.request_exec(cmd.as_ref()).map_err(|err| {
803 RemoteError::new_ex(
804 RemoteErrorType::ProtocolError,
805 format!("Could not execute command \"{}\": {err}", cmd.as_ref()),
806 )
807 })?;
808 debug!("Sending EOF");
810 channel.send_eof().map_err(|err| {
811 RemoteError::new_ex(
812 RemoteErrorType::ProtocolError,
813 format!("Could not send EOF: {err}"),
814 )
815 })?;
816
817 let mut output: String = String::new();
819 match channel.stdout().read_to_string(&mut output) {
820 Ok(_) => {
821 let res = channel.get_exit_status();
823 trace!("Command output (res: {res:?}): {output}");
824 Ok(output)
825 }
826 Err(err) => Err(RemoteError::new_ex(
827 RemoteErrorType::ProtocolError,
828 format!("Could not read output: {err}"),
829 )),
830 }
831}
832
833fn parse_scp_header_filesize(header: &[u8]) -> RemoteResult<usize> {
835 let header_str = std::str::from_utf8(header).map_err(|e| {
837 RemoteError::new_ex(
838 RemoteErrorType::ProtocolError,
839 format!("Could not parse header: {e}"),
840 )
841 })?;
842 let parts: Vec<&str> = header_str.split_whitespace().collect();
843 if parts.len() < 3 {
844 return Err(RemoteError::new_ex(
845 RemoteErrorType::ProtocolError,
846 "Invalid SCP header: not enough parts",
847 ));
848 }
849 if !parts[0].starts_with('C') {
850 return Err(RemoteError::new_ex(
851 RemoteErrorType::ProtocolError,
852 "Invalid SCP header: missing 'C'",
853 ));
854 }
855 let size = parts[1].parse::<usize>().map_err(|e| {
856 RemoteError::new_ex(
857 RemoteErrorType::ProtocolError,
858 format!("Invalid file size: {e}"),
859 )
860 })?;
861
862 Ok(size)
863}
864
865fn wait_for_ack(channel: &libssh_rs::Channel) -> RemoteResult<()> {
867 debug!("Waiting for channel acknowledgment");
868 let mut ack = [0u8; 1024];
870 let n = channel.stdout().read(&mut ack).map_err(|err| {
871 RemoteError::new_ex(
872 RemoteErrorType::ProtocolError,
873 format!("Could not read from channel: {err}"),
874 )
875 })?;
876 if n == 1 && ack[0] != 0 {
877 Err(RemoteError::new_ex(
878 RemoteErrorType::ProtocolError,
879 format!("Unexpected ACK: {ack:?} (read {n} bytes)"),
880 ))
881 } else {
882 Ok(())
883 }
884}