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            .canonicalize(conv_path_to_str(path))
461            .map(PathBuf::from)
462            .map_err(|err| {
463                RemoteError::new_ex(
464                    RemoteErrorType::ProtocolError,
465                    format!(
466                        "Could not resolve real path for '{path}': {err}",
467                        path = path.display()
468                    ),
469                )
470            })
471    }
472
473    fn rename(&self, src: &Path, dest: &Path) -> RemoteResult<()> {
474        self.inner
475            .rename(conv_path_to_str(src), conv_path_to_str(dest))
476            .map_err(|err| {
477                RemoteError::new_ex(
478                    RemoteErrorType::ProtocolError,
479                    format!("Could not rename file '{src}': {err}", src = src.display()),
480                )
481            })
482    }
483
484    fn rmdir(&self, path: &Path) -> RemoteResult<()> {
485        self.inner
486            .remove_dir(conv_path_to_str(path))
487            .map_err(|err| {
488                RemoteError::new_ex(
489                    RemoteErrorType::CouldNotRemoveFile,
490                    format!(
491                        "Could not remove directory '{path}': {err}",
492                        path = path.display()
493                    ),
494                )
495            })
496    }
497
498    fn setstat(&self, path: &Path, metadata: Metadata) -> RemoteResult<()> {
499        self.inner
500            .set_metadata(conv_path_to_str(path), &Self::set_attributes(metadata))
501            .map_err(|err| {
502                RemoteError::new_ex(
503                    RemoteErrorType::ProtocolError,
504                    format!(
505                        "Could not set file attributes for '{path}': {err}",
506                        path = path.display()
507                    ),
508                )
509            })
510    }
511
512    fn stat(&self, filename: &Path) -> RemoteResult<File> {
513        self.inner
514            .metadata(conv_path_to_str(filename))
515            .map(|metadata| self.make_fsentry(MakePath::File(filename), metadata))
516            .map_err(|err| {
517                RemoteError::new_ex(
518                    RemoteErrorType::ProtocolError,
519                    format!(
520                        "Could not get file attributes for '{filename}': {err}",
521                        filename = filename.display()
522                    ),
523                )
524            })
525    }
526
527    fn symlink(&self, path: &Path, target: &Path) -> RemoteResult<()> {
528        self.inner
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) => {
661                match self.inner.read_link(conv_path_to_str(&path)) {
662                    Ok(target) => Some(PathBuf::from(target)),
663                    Err(err) => {
664                        error!(
665                            "Failed to read link of {} (even it's supposed to be a symlink): {err}",
666                            path.display(),
667                        );
668                        None
669                    }
670                }
671            }
672            _ => None,
673        };
674        let file_type = if symlink.is_some() {
675            FileType::Symlink
676        } else if matches!(metadata.file_type(), Some(libssh_rs::FileType::Directory)) {
677            FileType::Directory
678        } else {
679            FileType::File
680        };
681        let entry_metadata = Metadata {
682            accessed,
683            created: None,
684            file_type,
685            gid,
686            mode,
687            modified,
688            size,
689            symlink,
690            uid,
691        };
692        trace!("Metadata for {}: {:?}", path.display(), entry_metadata);
693        File {
694            path: path.to_path_buf(),
695            metadata: entry_metadata,
696        }
697    }
698}
699
700fn authenticate(session: &mut libssh_rs::Session, opts: &SshOpts) -> RemoteResult<()> {
701    // parse configuration
702    let ssh_config = Config::try_from(opts)?;
703    let username = ssh_config.username.clone();
704
705    debug!("Authenticating to {}", opts.host);
706    session
707        .set_option(SshOption::User(Some(username)))
708        .map_err(|e| {
709            RemoteError::new_ex(
710                RemoteErrorType::AuthenticationFailed,
711                format!("Failed to set username: {e}"),
712            )
713        })?;
714
715    debug!("Trying with userauth_none");
716    match session.userauth_none(opts.username.as_deref()) {
717        Ok(AuthStatus::Success) => {
718            debug!("Authenticated with userauth_none");
719            return Ok(());
720        }
721        Ok(status) => {
722            debug!("userauth_none returned status: {status:?}");
723        }
724        Err(err) => {
725            debug!("userauth_none failed: {err}");
726        }
727    }
728
729    let auth_methods = session
730        .userauth_list(opts.username.as_deref())
731        .map_err(|e| RemoteError::new_ex(RemoteErrorType::AuthenticationFailed, e))?;
732    debug!("Available authentication methods: {auth_methods:?}");
733
734    if auth_methods.contains(AuthMethods::PUBLIC_KEY) {
735        debug!("Trying public key authentication");
736        // try with known key to config
737        match session.userauth_public_key_auto(None, None) {
738            Ok(AuthStatus::Success) => {
739                debug!("Authenticated with public key");
740                return Ok(());
741            }
742            Ok(status) => {
743                debug!("userauth_public_key_auto returned status: {status:?}");
744            }
745            Err(err) => {
746                debug!("userauth_public_key_auto failed: {err}");
747            }
748        }
749
750        // try with storage
751        match key_storage_auth(session, opts, &ssh_config) {
752            Ok(()) => {
753                debug!("Authenticated with public key from storage");
754                return Ok(());
755            }
756            Err(err) => {
757                debug!("Key storage authentication failed: {err}");
758            }
759        }
760    }
761
762    if auth_methods.contains(AuthMethods::PASSWORD) {
763        debug!("Trying password authentication");
764
765        // NOTE: you cannot pass password None. It causes SEGFAULT
766        match session.userauth_password(None, Some(opts.password.as_deref().unwrap_or_default())) {
767            Ok(AuthStatus::Success) => {
768                debug!("Authenticated with password");
769                return Ok(());
770            }
771            Ok(status) => {
772                debug!("userauth_password returned status: {status:?}");
773            }
774            Err(err) => {
775                debug!("userauth_password failed: {err}");
776            }
777        }
778    }
779
780    Err(RemoteError::new_ex(
781        RemoteErrorType::AuthenticationFailed,
782        "all authentication methods failed",
783    ))
784}
785
786fn key_storage_auth(
787    session: &mut libssh_rs::Session,
788    opts: &SshOpts,
789    ssh_config: &Config,
790) -> RemoteResult<()> {
791    let Some(key_storage) = &opts.key_storage else {
792        return Err(RemoteError::new_ex(
793            RemoteErrorType::AuthenticationFailed,
794            "no key storage available",
795        ));
796    };
797
798    let Some(priv_key_path) = key_storage
799        .resolve(&ssh_config.host, &ssh_config.username)
800        .or(key_storage.resolve(
801            ssh_config.resolved_host.as_str(),
802            ssh_config.username.as_str(),
803        ))
804    else {
805        return Err(RemoteError::new_ex(
806            RemoteErrorType::AuthenticationFailed,
807            "no key found in storage",
808        ));
809    };
810
811    let Ok(privkey) =
812        SshKey::from_privkey_file(conv_path_to_str(&priv_key_path), opts.password.as_deref())
813    else {
814        return Err(RemoteError::new_ex(
815            RemoteErrorType::AuthenticationFailed,
816            format!(
817                "could not load private key from file: {}",
818                priv_key_path.display()
819            ),
820        ));
821    };
822
823    match session
824        .userauth_publickey(opts.username.as_deref(), &privkey)
825        .map_err(|e| RemoteError::new_ex(RemoteErrorType::AuthenticationFailed, e))
826    {
827        Ok(AuthStatus::Success) => Ok(()),
828        Ok(status) => Err(RemoteError::new_ex(
829            RemoteErrorType::AuthenticationFailed,
830            format!("authentication failed: {status:?}"),
831        )),
832        Err(err) => Err(err),
833    }
834}
835
836fn perform_shell_cmd<S: AsRef<str>>(
837    session: &mut libssh_rs::Session,
838    cmd: S,
839) -> RemoteResult<String> {
840    // Create channel
841    trace!("Running command: {}", cmd.as_ref());
842    let channel = match session.new_channel() {
843        Ok(ch) => ch,
844        Err(err) => {
845            return Err(RemoteError::new_ex(
846                RemoteErrorType::ProtocolError,
847                format!("Could not open channel: {err}"),
848            ));
849        }
850    };
851
852    debug!("Opening channel session");
853    channel.open_session().map_err(|err| {
854        RemoteError::new_ex(
855            RemoteErrorType::ProtocolError,
856            format!("Could not open session: {err}"),
857        )
858    })?;
859
860    // escape single quotes in command
861    let cmd = cmd.as_ref().replace('\'', r#"'\''"#); // close, escape, and reopen
862
863    debug!("Requesting command execution: {cmd}",);
864    channel
865        .request_exec(&format!("sh -c '{cmd}'"))
866        .map_err(|err| {
867            RemoteError::new_ex(
868                RemoteErrorType::ProtocolError,
869                format!("Could not execute command \"{cmd}\": {err}"),
870            )
871        })?;
872    // send EOF
873    debug!("Sending EOF");
874    channel.send_eof().map_err(|err| {
875        RemoteError::new_ex(
876            RemoteErrorType::ProtocolError,
877            format!("Could not send EOF: {err}"),
878        )
879    })?;
880
881    // Read output
882    let mut output: String = String::new();
883    match channel.stdout().read_to_string(&mut output) {
884        Ok(_) => {
885            // Wait close
886            let res = channel.get_exit_status();
887            trace!("Command output (res: {res:?}): {output}");
888            Ok(output)
889        }
890        Err(err) => Err(RemoteError::new_ex(
891            RemoteErrorType::ProtocolError,
892            format!("Could not read output: {err}"),
893        )),
894    }
895}
896
897/// Read filesize from scp header
898fn parse_scp_header_filesize(header: &[u8]) -> RemoteResult<usize> {
899    // Header format: C<mode> <size> <filename>\n
900    let header_str = std::str::from_utf8(header).map_err(|e| {
901        RemoteError::new_ex(
902            RemoteErrorType::ProtocolError,
903            format!("Could not parse header: {e}"),
904        )
905    })?;
906    let parts: Vec<&str> = header_str.split_whitespace().collect();
907    if parts.len() < 3 {
908        return Err(RemoteError::new_ex(
909            RemoteErrorType::ProtocolError,
910            "Invalid SCP header: not enough parts",
911        ));
912    }
913    if !parts[0].starts_with('C') {
914        return Err(RemoteError::new_ex(
915            RemoteErrorType::ProtocolError,
916            "Invalid SCP header: missing 'C'",
917        ));
918    }
919    let size = parts[1].parse::<usize>().map_err(|e| {
920        RemoteError::new_ex(
921            RemoteErrorType::ProtocolError,
922            format!("Invalid file size: {e}"),
923        )
924    })?;
925
926    Ok(size)
927}
928
929/// Wait for channel ACK
930fn wait_for_ack(channel: &libssh_rs::Channel) -> RemoteResult<()> {
931    debug!("Waiting for channel acknowledgment");
932    // read ACK
933    let mut ack = [0u8; 1024];
934    let n = channel.stdout().read(&mut ack).map_err(|err| {
935        RemoteError::new_ex(
936            RemoteErrorType::ProtocolError,
937            format!("Could not read from channel: {err}"),
938        )
939    })?;
940    if n == 1 && ack[0] != 0 {
941        Err(RemoteError::new_ex(
942            RemoteErrorType::ProtocolError,
943            format!("Unexpected ACK: {ack:?} (read {n} bytes)"),
944        ))
945    } else {
946        Ok(())
947    }
948}