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