Skip to main content

remotefs_ssh/ssh/backend/
libssh.rs

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
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
335/// Number of bytes per SFTP read call for buffered reads.
336///
337/// libssh caps each `sftp_read` at the server's maximum packet payload
338/// (typically 64 KiB). Using a larger request size lets the C library
339/// issue fewer round-trips when possible, while still working correctly
340/// when the server returns less.
341const 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
363/// A seekable, in-memory read buffer wrapping file data fetched via SFTP.
364struct 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        //panic!("Figa");
418
419        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
560/// Reads an entire remote file into memory using a large buffer to minimize
561/// SFTP round-trips.
562///
563/// Each `sftp_read` call in libssh is a synchronous request-response cycle,
564/// and file handles from the same session serialize through a mutex, so true
565/// pipelining is not possible without the AIO FFI. Reading with a large
566/// buffer (256 KiB) reduces the number of round-trips compared to the
567/// default 64 KiB reads the caller would otherwise perform.
568fn 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        // parse metadata
656        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    // parse configuration
703    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        // try with known key to config
738        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        // try with storage
752        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        // NOTE: you cannot pass password None. It causes SEGFAULT
767        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    // Create channel
842    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    // escape single quotes in command
862    let cmd = cmd.as_ref().replace('\'', r#"'\''"#); // close, escape, and reopen
863
864    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    // send EOF
874    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    // Read output
883    let mut output: String = String::new();
884    match channel.stdout().read_to_string(&mut output) {
885        Ok(_) => {
886            // Wait close
887            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
898/// Read filesize from scp header
899fn parse_scp_header_filesize(header: &[u8]) -> RemoteResult<usize> {
900    // Header format: C<mode> <size> <filename>\n
901    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
930/// Wait for channel ACK
931fn wait_for_ack(channel: &libssh_rs::Channel) -> RemoteResult<()> {
932    debug!("Waiting for channel acknowledgment");
933    // read ACK
934    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}