remotefs_ssh/ssh/backend/
libssh.rs

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
16/// An implementation of [`SshSession`] using libssh as the backend.
17///
18/// See <https://docs.rs/libssh-rs/0.3.6/libssh_rs/struct.Session.html>
19pub struct LibSshSession {
20    session: libssh_rs::Session,
21}
22
23/// A wrapper around [`libssh_rs::Sftp`] to provide a SFTP client for [`LibSshSession`]
24///
25/// See <https://docs.rs/libssh-rs/0.3.6/libssh_rs/struct.Sftp.html>
26pub struct LibSshSftp {
27    inner: libssh_rs::Sftp,
28}
29
30/// A wrapper around [`libssh_rs::Channel`] to provide a SCP recv channel for [`LibSshSession`]
31struct ScpRecvChannel {
32    channel: libssh_rs::Channel,
33    /// We must keep track of the total file size
34    /// otherwise read will hang
35    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        // read up to
46        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
66/// A wrapper around [`libssh_rs::Channel`] to provide a SCP send channel for [`LibSshSession`]
67struct 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        // Resolve host
98        debug!("Connecting to '{}'", opts.host);
99
100        // Create session
101        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        // set hostname
110        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        // set methods
127        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        // Open connection and initialize handshake
135        if let Err(err) = session.connect() {
136            error!("SSH handshake failed: {err}");
137            return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
138        }
139
140        // try to authenticate userauth_none
141        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        // open channel
201        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        // exec `scp -f %s`
216        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        // write \0
225        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        // read header
233        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        // read filesize from header
242        let filesize = parse_scp_header_filesize(&header[..bytes])?;
243        debug!("File size: {filesize}");
244        // send OK
245        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        // open channel
273        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        // exec `scp -t %s`
288        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
297        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        // send file header
307        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
320        wait_for_ack(&channel)?;
321
322        // return channel
323        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        //panic!("Figa");
417
418        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        // parse metadata
596        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    // parse configuration
643    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        // try with known key to config
678        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        // try with storage
692        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        // NOTE: you cannot pass password None. It causes SEGFAULT
707        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    // Create channel
782    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    // send EOF
809    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    // Read output
818    let mut output: String = String::new();
819    match channel.stdout().read_to_string(&mut output) {
820        Ok(_) => {
821            // Wait close
822            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
833/// Read filesize from scp header
834fn parse_scp_header_filesize(header: &[u8]) -> RemoteResult<usize> {
835    // Header format: C<mode> <size> <filename>\n
836    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
865/// Wait for channel ACK
866fn wait_for_ack(channel: &libssh_rs::Channel) -> RemoteResult<()> {
867    debug!("Waiting for channel acknowledgment");
868    // read ACK
869    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}