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