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 .canonicalize(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
534 .symlink(conv_path_to_str(target), conv_path_to_str(path))
535 .map_err(|err| {
536 RemoteError::new_ex(
537 RemoteErrorType::FileCreateDenied,
538 format!(
539 "Could not create symlink '{path}': {err}",
540 path = path.display()
541 ),
542 )
543 })
544 }
545
546 fn unlink(&self, path: &Path) -> RemoteResult<()> {
547 self.inner
548 .remove_file(conv_path_to_str(path))
549 .map_err(|err| {
550 RemoteError::new_ex(
551 RemoteErrorType::CouldNotRemoveFile,
552 format!(
553 "Could not remove file '{path}': {err}",
554 path = path.display()
555 ),
556 )
557 })
558 }
559}
560
561fn conv_path_to_str(path: &Path) -> &str {
562 path.to_str().unwrap_or_default()
563}
564
565fn buffered_sftp_read(sftp: &libssh_rs::Sftp, path: &Path) -> RemoteResult<Vec<u8>> {
574 let path_str = conv_path_to_str(path);
575
576 let file_size = sftp
577 .metadata(path_str)
578 .map(|m| m.len().unwrap_or(0) as usize)
579 .map_err(|err| {
580 RemoteError::new_ex(
581 RemoteErrorType::ProtocolError,
582 format!("Could not stat '{path}': {err}", path = path.display()),
583 )
584 })?;
585
586 if file_size == 0 {
587 return Ok(Vec::new());
588 }
589
590 let mut file = sftp
591 .open(path_str, OpenFlags::READ_ONLY, 0)
592 .map_err(|err| {
593 RemoteError::new_ex(
594 RemoteErrorType::ProtocolError,
595 format!(
596 "Could not open file at '{path}': {err}",
597 path = path.display()
598 ),
599 )
600 })?;
601
602 let mut data = Vec::with_capacity(file_size);
603 let mut buf = [0_u8; SFTP_READ_BUF_SIZE];
604
605 loop {
606 let n = file.read(&mut buf).map_err(|err| {
607 RemoteError::new_ex(
608 RemoteErrorType::IoError,
609 format!("Failed to read file '{path}': {err}", path = path.display()),
610 )
611 })?;
612 if n == 0 {
613 break;
614 }
615 data.extend_from_slice(&buf[..n]);
616 }
617
618 Ok(data)
619}
620
621enum MakePath<'a> {
622 Directory(&'a Path),
623 File(&'a Path),
624}
625
626impl LibSshSftp {
627 fn set_attributes(metadata: Metadata) -> libssh_rs::SetAttributes {
628 let atime = metadata.accessed.unwrap_or(UNIX_EPOCH);
629 let mtime = metadata.modified.unwrap_or(UNIX_EPOCH);
630
631 let uid_gid = match (metadata.uid, metadata.gid) {
632 (Some(uid), Some(gid)) => Some((uid, gid)),
633 _ => None,
634 };
635
636 libssh_rs::SetAttributes {
637 size: Some(metadata.size),
638 uid_gid,
639 permissions: metadata.mode.map(|m| m.into()),
640 atime_mtime: Some((atime, mtime)),
641 }
642 }
643
644 fn make_fsentry(&self, path: MakePath<'_>, metadata: libssh_rs::Metadata) -> File {
645 let name = match metadata.name() {
646 None => "/".to_string(),
647 Some(name) => name.to_string(),
648 };
649 debug!("Found file {name}");
650
651 let path = match path {
652 MakePath::Directory(dir) => dir.join(&name),
653 MakePath::File(file) => file.to_path_buf(),
654 };
655 debug!("Computed path for {name}: {path}", path = path.display());
656
657 let uid = metadata.uid();
659 let gid = metadata.gid();
660 let mode = metadata.permissions().map(UnixPex::from);
661 let size = metadata.len().unwrap_or(0);
662 let accessed = metadata.accessed();
663 let modified = metadata.modified();
664 let symlink = match metadata.file_type() {
665 Some(libssh_rs::FileType::Symlink) => {
666 match self.inner.read_link(conv_path_to_str(&path)) {
667 Ok(target) => Some(PathBuf::from(target)),
668 Err(err) => {
669 error!(
670 "Failed to read link of {} (even it's supposed to be a symlink): {err}",
671 path.display(),
672 );
673 None
674 }
675 }
676 }
677 _ => None,
678 };
679 let file_type = if symlink.is_some() {
680 FileType::Symlink
681 } else if matches!(metadata.file_type(), Some(libssh_rs::FileType::Directory)) {
682 FileType::Directory
683 } else {
684 FileType::File
685 };
686 let entry_metadata = Metadata {
687 accessed,
688 created: None,
689 file_type,
690 gid,
691 mode,
692 modified,
693 size,
694 symlink,
695 uid,
696 };
697 trace!("Metadata for {}: {:?}", path.display(), entry_metadata);
698 File {
699 path: path.to_path_buf(),
700 metadata: entry_metadata,
701 }
702 }
703}
704
705fn authenticate(session: &mut libssh_rs::Session, opts: &SshOpts) -> RemoteResult<()> {
706 let ssh_config = Config::try_from(opts)?;
708 let username = ssh_config.username.clone();
709
710 debug!("Authenticating to {}", opts.host);
711 session
712 .set_option(SshOption::User(Some(username)))
713 .map_err(|e| {
714 RemoteError::new_ex(
715 RemoteErrorType::AuthenticationFailed,
716 format!("Failed to set username: {e}"),
717 )
718 })?;
719
720 debug!("Trying with userauth_none");
721 match session.userauth_none(opts.username.as_deref()) {
722 Ok(AuthStatus::Success) => {
723 debug!("Authenticated with userauth_none");
724 return Ok(());
725 }
726 Ok(status) => {
727 debug!("userauth_none returned status: {status:?}");
728 }
729 Err(err) => {
730 debug!("userauth_none failed: {err}");
731 }
732 }
733
734 let auth_methods = session
735 .userauth_list(opts.username.as_deref())
736 .map_err(|e| RemoteError::new_ex(RemoteErrorType::AuthenticationFailed, e))?;
737 debug!("Available authentication methods: {auth_methods:?}");
738
739 if auth_methods.contains(AuthMethods::PUBLIC_KEY) {
740 debug!("Trying public key authentication");
741 match session.userauth_public_key_auto(None, None) {
743 Ok(AuthStatus::Success) => {
744 debug!("Authenticated with public key");
745 return Ok(());
746 }
747 Ok(status) => {
748 debug!("userauth_public_key_auto returned status: {status:?}");
749 }
750 Err(err) => {
751 debug!("userauth_public_key_auto failed: {err}");
752 }
753 }
754
755 match key_storage_auth(session, opts, &ssh_config) {
757 Ok(()) => {
758 debug!("Authenticated with public key from storage");
759 return Ok(());
760 }
761 Err(err) => {
762 debug!("Key storage authentication failed: {err}");
763 }
764 }
765 }
766
767 if auth_methods.contains(AuthMethods::PASSWORD) {
768 debug!("Trying password authentication");
769
770 match session.userauth_password(None, Some(opts.password.as_deref().unwrap_or_default())) {
772 Ok(AuthStatus::Success) => {
773 debug!("Authenticated with password");
774 return Ok(());
775 }
776 Ok(status) => {
777 debug!("userauth_password returned status: {status:?}");
778 }
779 Err(err) => {
780 debug!("userauth_password failed: {err}");
781 }
782 }
783 }
784
785 Err(RemoteError::new_ex(
786 RemoteErrorType::AuthenticationFailed,
787 "all authentication methods failed",
788 ))
789}
790
791fn key_storage_auth(
792 session: &mut libssh_rs::Session,
793 opts: &SshOpts,
794 ssh_config: &Config,
795) -> RemoteResult<()> {
796 let Some(key_storage) = &opts.key_storage else {
797 return Err(RemoteError::new_ex(
798 RemoteErrorType::AuthenticationFailed,
799 "no key storage available",
800 ));
801 };
802
803 let Some(priv_key_path) = key_storage
804 .resolve(&ssh_config.host, &ssh_config.username)
805 .or(key_storage.resolve(
806 ssh_config.resolved_host.as_str(),
807 ssh_config.username.as_str(),
808 ))
809 else {
810 return Err(RemoteError::new_ex(
811 RemoteErrorType::AuthenticationFailed,
812 "no key found in storage",
813 ));
814 };
815
816 let Ok(privkey) =
817 SshKey::from_privkey_file(conv_path_to_str(&priv_key_path), opts.password.as_deref())
818 else {
819 return Err(RemoteError::new_ex(
820 RemoteErrorType::AuthenticationFailed,
821 format!(
822 "could not load private key from file: {}",
823 priv_key_path.display()
824 ),
825 ));
826 };
827
828 match session
829 .userauth_publickey(opts.username.as_deref(), &privkey)
830 .map_err(|e| RemoteError::new_ex(RemoteErrorType::AuthenticationFailed, e))
831 {
832 Ok(AuthStatus::Success) => Ok(()),
833 Ok(status) => Err(RemoteError::new_ex(
834 RemoteErrorType::AuthenticationFailed,
835 format!("authentication failed: {status:?}"),
836 )),
837 Err(err) => Err(err),
838 }
839}
840
841fn perform_shell_cmd<S: AsRef<str>>(
842 session: &mut libssh_rs::Session,
843 cmd: S,
844) -> RemoteResult<String> {
845 trace!("Running command: {}", cmd.as_ref());
847 let channel = match session.new_channel() {
848 Ok(ch) => ch,
849 Err(err) => {
850 return Err(RemoteError::new_ex(
851 RemoteErrorType::ProtocolError,
852 format!("Could not open channel: {err}"),
853 ));
854 }
855 };
856
857 debug!("Opening channel session");
858 channel.open_session().map_err(|err| {
859 RemoteError::new_ex(
860 RemoteErrorType::ProtocolError,
861 format!("Could not open session: {err}"),
862 )
863 })?;
864
865 let cmd = cmd.as_ref().replace('\'', r#"'\''"#); debug!("Requesting command execution: {cmd}",);
869 channel
870 .request_exec(&format!("sh -c '{cmd}'"))
871 .map_err(|err| {
872 RemoteError::new_ex(
873 RemoteErrorType::ProtocolError,
874 format!("Could not execute command \"{cmd}\": {err}"),
875 )
876 })?;
877 debug!("Sending EOF");
879 channel.send_eof().map_err(|err| {
880 RemoteError::new_ex(
881 RemoteErrorType::ProtocolError,
882 format!("Could not send EOF: {err}"),
883 )
884 })?;
885
886 let mut output: String = String::new();
888 match channel.stdout().read_to_string(&mut output) {
889 Ok(_) => {
890 let res = channel.get_exit_status();
892 trace!("Command output (res: {res:?}): {output}");
893 Ok(output)
894 }
895 Err(err) => Err(RemoteError::new_ex(
896 RemoteErrorType::ProtocolError,
897 format!("Could not read output: {err}"),
898 )),
899 }
900}
901
902fn parse_scp_header_filesize(header: &[u8]) -> RemoteResult<usize> {
904 let header_str = std::str::from_utf8(header).map_err(|e| {
906 RemoteError::new_ex(
907 RemoteErrorType::ProtocolError,
908 format!("Could not parse header: {e}"),
909 )
910 })?;
911 let parts: Vec<&str> = header_str.split_whitespace().collect();
912 if parts.len() < 3 {
913 return Err(RemoteError::new_ex(
914 RemoteErrorType::ProtocolError,
915 "Invalid SCP header: not enough parts",
916 ));
917 }
918 if !parts[0].starts_with('C') {
919 return Err(RemoteError::new_ex(
920 RemoteErrorType::ProtocolError,
921 "Invalid SCP header: missing 'C'",
922 ));
923 }
924 let size = parts[1].parse::<usize>().map_err(|e| {
925 RemoteError::new_ex(
926 RemoteErrorType::ProtocolError,
927 format!("Invalid file size: {e}"),
928 )
929 })?;
930
931 Ok(size)
932}
933
934fn wait_for_ack(channel: &libssh_rs::Channel) -> RemoteResult<()> {
936 debug!("Waiting for channel acknowledgment");
937 let mut ack = [0u8; 1024];
939 let n = channel.stdout().read(&mut ack).map_err(|err| {
940 RemoteError::new_ex(
941 RemoteErrorType::ProtocolError,
942 format!("Could not read from channel: {err}"),
943 )
944 })?;
945 if n == 1 && ack[0] != 0 {
946 Err(RemoteError::new_ex(
947 RemoteErrorType::ProtocolError,
948 format!("Unexpected ACK: {ack:?} (read {n} bytes)"),
949 ))
950 } else {
951 Ok(())
952 }
953}