1use std::io::{Cursor, 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
335const SFTP_READ_BUF_SIZE: usize = 256 * 1024;
342
343struct SftpFileWriter(libssh_rs::SftpFile);
344
345impl Write for SftpFileWriter {
346 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
347 self.0.write(buf)
348 }
349
350 fn flush(&mut self) -> std::io::Result<()> {
351 self.0.flush()
352 }
353}
354
355impl Seek for SftpFileWriter {
356 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
357 self.0.seek(pos)
358 }
359}
360
361impl WriteAndSeek for SftpFileWriter {}
362
363struct BufferedSftpReader(Cursor<Vec<u8>>);
365
366impl Read for BufferedSftpReader {
367 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
368 self.0.read(buf)
369 }
370}
371
372impl Seek for BufferedSftpReader {
373 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
374 self.0.seek(pos)
375 }
376}
377
378impl ReadAndSeek for BufferedSftpReader {}
379
380impl Sftp for LibSshSftp {
381 fn mkdir(&self, path: &Path, mode: i32) -> RemoteResult<()> {
382 self.inner
383 .create_dir(conv_path_to_str(path), mode as u32)
384 .map_err(|err| {
385 RemoteError::new_ex(
386 RemoteErrorType::FileCreateDenied,
387 format!(
388 "Could not create directory '{path}': {err}",
389 path = path.display()
390 ),
391 )
392 })
393 }
394
395 fn open_read(&self, path: &Path) -> RemoteResult<ReadStream> {
396 let data = buffered_sftp_read(&self.inner, path)?;
397 Ok(ReadStream::from(
398 Box::new(BufferedSftpReader(Cursor::new(data))) as Box<dyn ReadAndSeek>,
399 ))
400 }
401
402 fn open_write(
403 &self,
404 path: &Path,
405 flags: super::WriteMode,
406 mode: i32,
407 ) -> RemoteResult<WriteStream> {
408 let flags = match flags {
409 super::WriteMode::Append => {
410 OpenFlags::WRITE_ONLY | OpenFlags::APPEND | OpenFlags::CREATE
411 }
412 super::WriteMode::Truncate => {
413 OpenFlags::WRITE_ONLY | OpenFlags::CREATE | OpenFlags::TRUNCATE
414 }
415 };
416
417 self.inner
420 .open(conv_path_to_str(path), flags, mode as u32)
421 .map(|file| WriteStream::from(Box::new(SftpFileWriter(file)) as Box<dyn WriteAndSeek>))
422 .map_err(|err| {
423 RemoteError::new_ex(
424 RemoteErrorType::ProtocolError,
425 format!(
426 "Could not open file at '{path}': {err}",
427 path = path.display()
428 ),
429 )
430 })
431 }
432
433 fn readdir<T>(&self, dirname: T) -> RemoteResult<Vec<remotefs::File>>
434 where
435 T: AsRef<Path>,
436 {
437 self.inner
438 .read_dir(conv_path_to_str(dirname.as_ref()))
439 .map(|files| {
440 files
441 .into_iter()
442 .filter(|metadata| {
443 metadata.name() != Some(".") && metadata.name() != Some("..")
444 })
445 .map(|metadata| {
446 self.make_fsentry(MakePath::Directory(dirname.as_ref()), metadata)
447 })
448 .collect()
449 })
450 .map_err(|err| {
451 RemoteError::new_ex(
452 RemoteErrorType::ProtocolError,
453 format!("Could not read directory: {err}",),
454 )
455 })
456 }
457
458 fn realpath(&self, path: &Path) -> RemoteResult<PathBuf> {
459 self.inner
460 .read_link(conv_path_to_str(path))
461 .map(PathBuf::from)
462 .map_err(|err| {
463 RemoteError::new_ex(
464 RemoteErrorType::ProtocolError,
465 format!(
466 "Could not resolve real path for '{path}': {err}",
467 path = path.display()
468 ),
469 )
470 })
471 }
472
473 fn rename(&self, src: &Path, dest: &Path) -> RemoteResult<()> {
474 self.inner
475 .rename(conv_path_to_str(src), conv_path_to_str(dest))
476 .map_err(|err| {
477 RemoteError::new_ex(
478 RemoteErrorType::ProtocolError,
479 format!("Could not rename file '{src}': {err}", src = src.display()),
480 )
481 })
482 }
483
484 fn rmdir(&self, path: &Path) -> RemoteResult<()> {
485 self.inner
486 .remove_dir(conv_path_to_str(path))
487 .map_err(|err| {
488 RemoteError::new_ex(
489 RemoteErrorType::CouldNotRemoveFile,
490 format!(
491 "Could not remove directory '{path}': {err}",
492 path = path.display()
493 ),
494 )
495 })
496 }
497
498 fn setstat(&self, path: &Path, metadata: Metadata) -> RemoteResult<()> {
499 self.inner
500 .set_metadata(conv_path_to_str(path), &Self::set_attributes(metadata))
501 .map_err(|err| {
502 RemoteError::new_ex(
503 RemoteErrorType::ProtocolError,
504 format!(
505 "Could not set file attributes for '{path}': {err}",
506 path = path.display()
507 ),
508 )
509 })
510 }
511
512 fn stat(&self, filename: &Path) -> RemoteResult<File> {
513 self.inner
514 .metadata(conv_path_to_str(filename))
515 .map(|metadata| self.make_fsentry(MakePath::File(filename), metadata))
516 .map_err(|err| {
517 RemoteError::new_ex(
518 RemoteErrorType::ProtocolError,
519 format!(
520 "Could not get file attributes for '{filename}': {err}",
521 filename = filename.display()
522 ),
523 )
524 })
525 }
526
527 fn symlink(&self, path: &Path, target: &Path) -> RemoteResult<()> {
528 self.inner
529 .symlink(conv_path_to_str(path), conv_path_to_str(target))
530 .map_err(|err| {
531 RemoteError::new_ex(
532 RemoteErrorType::FileCreateDenied,
533 format!(
534 "Could not create symlink '{path}': {err}",
535 path = path.display()
536 ),
537 )
538 })
539 }
540
541 fn unlink(&self, path: &Path) -> RemoteResult<()> {
542 self.inner
543 .remove_file(conv_path_to_str(path))
544 .map_err(|err| {
545 RemoteError::new_ex(
546 RemoteErrorType::CouldNotRemoveFile,
547 format!(
548 "Could not remove file '{path}': {err}",
549 path = path.display()
550 ),
551 )
552 })
553 }
554}
555
556fn conv_path_to_str(path: &Path) -> &str {
557 path.to_str().unwrap_or_default()
558}
559
560fn buffered_sftp_read(sftp: &libssh_rs::Sftp, path: &Path) -> RemoteResult<Vec<u8>> {
569 let path_str = conv_path_to_str(path);
570
571 let file_size = sftp
572 .metadata(path_str)
573 .map(|m| m.len().unwrap_or(0) as usize)
574 .map_err(|err| {
575 RemoteError::new_ex(
576 RemoteErrorType::ProtocolError,
577 format!("Could not stat '{path}': {err}", path = path.display()),
578 )
579 })?;
580
581 if file_size == 0 {
582 return Ok(Vec::new());
583 }
584
585 let mut file = sftp
586 .open(path_str, OpenFlags::READ_ONLY, 0)
587 .map_err(|err| {
588 RemoteError::new_ex(
589 RemoteErrorType::ProtocolError,
590 format!(
591 "Could not open file at '{path}': {err}",
592 path = path.display()
593 ),
594 )
595 })?;
596
597 let mut data = Vec::with_capacity(file_size);
598 let mut buf = [0_u8; SFTP_READ_BUF_SIZE];
599
600 loop {
601 let n = file.read(&mut buf).map_err(|err| {
602 RemoteError::new_ex(
603 RemoteErrorType::IoError,
604 format!("Failed to read file '{path}': {err}", path = path.display()),
605 )
606 })?;
607 if n == 0 {
608 break;
609 }
610 data.extend_from_slice(&buf[..n]);
611 }
612
613 Ok(data)
614}
615
616enum MakePath<'a> {
617 Directory(&'a Path),
618 File(&'a Path),
619}
620
621impl LibSshSftp {
622 fn set_attributes(metadata: Metadata) -> libssh_rs::SetAttributes {
623 let atime = metadata.accessed.unwrap_or(UNIX_EPOCH);
624 let mtime = metadata.modified.unwrap_or(UNIX_EPOCH);
625
626 let uid_gid = match (metadata.uid, metadata.gid) {
627 (Some(uid), Some(gid)) => Some((uid, gid)),
628 _ => None,
629 };
630
631 libssh_rs::SetAttributes {
632 size: Some(metadata.size),
633 uid_gid,
634 permissions: metadata.mode.map(|m| m.into()),
635 atime_mtime: Some((atime, mtime)),
636 }
637 }
638
639 fn make_fsentry(&self, path: MakePath<'_>, metadata: libssh_rs::Metadata) -> File {
640 let name = match metadata.name() {
641 None => "/".to_string(),
642 Some(name) => name.to_string(),
643 };
644 debug!("Found file {name}");
645
646 let path = match path {
647 MakePath::Directory(dir) => dir.join(&name),
648 MakePath::File(file) => file.to_path_buf(),
649 };
650 debug!("Computed path for {name}: {path}", path = path.display());
651
652 let uid = metadata.uid();
654 let gid = metadata.gid();
655 let mode = metadata.permissions().map(UnixPex::from);
656 let size = metadata.len().unwrap_or(0);
657 let accessed = metadata.accessed();
658 let modified = metadata.modified();
659 let symlink = match metadata.file_type() {
660 Some(libssh_rs::FileType::Symlink) => match self.realpath(&path) {
661 Ok(p) => Some(p),
662 Err(err) => {
663 error!(
664 "Failed to read link of {} (even it's supposed to be a symlink): {err}",
665 path.display(),
666 );
667 None
668 }
669 },
670 _ => None,
671 };
672 let file_type = if symlink.is_some() {
673 FileType::Symlink
674 } else if matches!(metadata.file_type(), Some(libssh_rs::FileType::Directory)) {
675 FileType::Directory
676 } else {
677 FileType::File
678 };
679 let entry_metadata = Metadata {
680 accessed,
681 created: None,
682 file_type,
683 gid,
684 mode,
685 modified,
686 size,
687 symlink,
688 uid,
689 };
690 trace!("Metadata for {}: {:?}", path.display(), entry_metadata);
691 File {
692 path: path.to_path_buf(),
693 metadata: entry_metadata,
694 }
695 }
696}
697
698fn authenticate(session: &mut libssh_rs::Session, opts: &SshOpts) -> RemoteResult<()> {
699 let ssh_config = Config::try_from(opts)?;
701 let username = ssh_config.username.clone();
702
703 debug!("Authenticating to {}", opts.host);
704 session
705 .set_option(SshOption::User(Some(username)))
706 .map_err(|e| {
707 RemoteError::new_ex(
708 RemoteErrorType::AuthenticationFailed,
709 format!("Failed to set username: {e}"),
710 )
711 })?;
712
713 debug!("Trying with userauth_none");
714 match session.userauth_none(opts.username.as_deref()) {
715 Ok(AuthStatus::Success) => {
716 debug!("Authenticated with userauth_none");
717 return Ok(());
718 }
719 Ok(status) => {
720 debug!("userauth_none returned status: {status:?}");
721 }
722 Err(err) => {
723 debug!("userauth_none failed: {err}");
724 }
725 }
726
727 let auth_methods = session
728 .userauth_list(opts.username.as_deref())
729 .map_err(|e| RemoteError::new_ex(RemoteErrorType::AuthenticationFailed, e))?;
730 debug!("Available authentication methods: {auth_methods:?}");
731
732 if auth_methods.contains(AuthMethods::PUBLIC_KEY) {
733 debug!("Trying public key authentication");
734 match session.userauth_public_key_auto(None, None) {
736 Ok(AuthStatus::Success) => {
737 debug!("Authenticated with public key");
738 return Ok(());
739 }
740 Ok(status) => {
741 debug!("userauth_public_key_auto returned status: {status:?}");
742 }
743 Err(err) => {
744 debug!("userauth_public_key_auto failed: {err}");
745 }
746 }
747
748 match key_storage_auth(session, opts, &ssh_config) {
750 Ok(()) => {
751 debug!("Authenticated with public key from storage");
752 return Ok(());
753 }
754 Err(err) => {
755 debug!("Key storage authentication failed: {err}");
756 }
757 }
758 }
759
760 if auth_methods.contains(AuthMethods::PASSWORD) {
761 debug!("Trying password authentication");
762
763 match session.userauth_password(None, Some(opts.password.as_deref().unwrap_or_default())) {
765 Ok(AuthStatus::Success) => {
766 debug!("Authenticated with password");
767 return Ok(());
768 }
769 Ok(status) => {
770 debug!("userauth_password returned status: {status:?}");
771 }
772 Err(err) => {
773 debug!("userauth_password failed: {err}");
774 }
775 }
776 }
777
778 Err(RemoteError::new_ex(
779 RemoteErrorType::AuthenticationFailed,
780 "all authentication methods failed",
781 ))
782}
783
784fn key_storage_auth(
785 session: &mut libssh_rs::Session,
786 opts: &SshOpts,
787 ssh_config: &Config,
788) -> RemoteResult<()> {
789 let Some(key_storage) = &opts.key_storage else {
790 return Err(RemoteError::new_ex(
791 RemoteErrorType::AuthenticationFailed,
792 "no key storage available",
793 ));
794 };
795
796 let Some(priv_key_path) = key_storage
797 .resolve(&ssh_config.host, &ssh_config.username)
798 .or(key_storage.resolve(
799 ssh_config.resolved_host.as_str(),
800 ssh_config.username.as_str(),
801 ))
802 else {
803 return Err(RemoteError::new_ex(
804 RemoteErrorType::AuthenticationFailed,
805 "no key found in storage",
806 ));
807 };
808
809 let Ok(privkey) =
810 SshKey::from_privkey_file(conv_path_to_str(&priv_key_path), opts.password.as_deref())
811 else {
812 return Err(RemoteError::new_ex(
813 RemoteErrorType::AuthenticationFailed,
814 format!(
815 "could not load private key from file: {}",
816 priv_key_path.display()
817 ),
818 ));
819 };
820
821 match session
822 .userauth_publickey(opts.username.as_deref(), &privkey)
823 .map_err(|e| RemoteError::new_ex(RemoteErrorType::AuthenticationFailed, e))
824 {
825 Ok(AuthStatus::Success) => Ok(()),
826 Ok(status) => Err(RemoteError::new_ex(
827 RemoteErrorType::AuthenticationFailed,
828 format!("authentication failed: {status:?}"),
829 )),
830 Err(err) => Err(err),
831 }
832}
833
834fn perform_shell_cmd<S: AsRef<str>>(
835 session: &mut libssh_rs::Session,
836 cmd: S,
837) -> RemoteResult<String> {
838 trace!("Running command: {}", cmd.as_ref());
840 let channel = match session.new_channel() {
841 Ok(ch) => ch,
842 Err(err) => {
843 return Err(RemoteError::new_ex(
844 RemoteErrorType::ProtocolError,
845 format!("Could not open channel: {err}"),
846 ));
847 }
848 };
849
850 debug!("Opening channel session");
851 channel.open_session().map_err(|err| {
852 RemoteError::new_ex(
853 RemoteErrorType::ProtocolError,
854 format!("Could not open session: {err}"),
855 )
856 })?;
857
858 let cmd = cmd.as_ref().replace('\'', r#"'\''"#); debug!("Requesting command execution: {cmd}",);
862 channel
863 .request_exec(&format!("sh -c '{cmd}'"))
864 .map_err(|err| {
865 RemoteError::new_ex(
866 RemoteErrorType::ProtocolError,
867 format!("Could not execute command \"{cmd}\": {err}"),
868 )
869 })?;
870 debug!("Sending EOF");
872 channel.send_eof().map_err(|err| {
873 RemoteError::new_ex(
874 RemoteErrorType::ProtocolError,
875 format!("Could not send EOF: {err}"),
876 )
877 })?;
878
879 let mut output: String = String::new();
881 match channel.stdout().read_to_string(&mut output) {
882 Ok(_) => {
883 let res = channel.get_exit_status();
885 trace!("Command output (res: {res:?}): {output}");
886 Ok(output)
887 }
888 Err(err) => Err(RemoteError::new_ex(
889 RemoteErrorType::ProtocolError,
890 format!("Could not read output: {err}"),
891 )),
892 }
893}
894
895fn parse_scp_header_filesize(header: &[u8]) -> RemoteResult<usize> {
897 let header_str = std::str::from_utf8(header).map_err(|e| {
899 RemoteError::new_ex(
900 RemoteErrorType::ProtocolError,
901 format!("Could not parse header: {e}"),
902 )
903 })?;
904 let parts: Vec<&str> = header_str.split_whitespace().collect();
905 if parts.len() < 3 {
906 return Err(RemoteError::new_ex(
907 RemoteErrorType::ProtocolError,
908 "Invalid SCP header: not enough parts",
909 ));
910 }
911 if !parts[0].starts_with('C') {
912 return Err(RemoteError::new_ex(
913 RemoteErrorType::ProtocolError,
914 "Invalid SCP header: missing 'C'",
915 ));
916 }
917 let size = parts[1].parse::<usize>().map_err(|e| {
918 RemoteError::new_ex(
919 RemoteErrorType::ProtocolError,
920 format!("Invalid file size: {e}"),
921 )
922 })?;
923
924 Ok(size)
925}
926
927fn wait_for_ack(channel: &libssh_rs::Channel) -> RemoteResult<()> {
929 debug!("Waiting for channel acknowledgment");
930 let mut ack = [0u8; 1024];
932 let n = channel.stdout().read(&mut ack).map_err(|err| {
933 RemoteError::new_ex(
934 RemoteErrorType::ProtocolError,
935 format!("Could not read from channel: {err}"),
936 )
937 })?;
938 if n == 1 && ack[0] != 0 {
939 Err(RemoteError::new_ex(
940 RemoteErrorType::ProtocolError,
941 format!("Unexpected ACK: {ack:?} (read {n} bytes)"),
942 ))
943 } else {
944 Ok(())
945 }
946}