remotefs_ssh/ssh/backend/
libssh2.rs

1use std::io::{Read, Seek, Write};
2use std::net::{SocketAddr, TcpStream, ToSocketAddrs as _};
3use std::path::{Path, PathBuf};
4use std::str::FromStr as _;
5use std::time::{Duration, SystemTime};
6
7use remotefs::fs::stream::{ReadAndSeek, WriteAndSeek};
8use remotefs::fs::{FileType, Metadata, ReadStream, UnixPex, WriteStream};
9use remotefs::{File, RemoteError, RemoteErrorType, RemoteResult};
10use ssh2::{FileStat, OpenType, RenameFlags};
11
12use super::SshSession;
13use crate::ssh::backend::Sftp;
14use crate::ssh::config::Config;
15use crate::{SshAgentIdentity, SshOpts};
16
17/// An implementation of [`SshSession`] using libssh2 as the backend.
18pub struct LibSsh2Session {
19    session: ssh2::Session,
20}
21
22/// A wrapper around [`ssh2::Sftp`] to provide a SFTP client for [`LibSsh2Session`]
23pub struct LibSsh2Sftp {
24    inner: ssh2::Sftp,
25}
26
27/// Authentication method
28#[derive(Debug, Clone, PartialEq, Eq)]
29enum Authentication {
30    RsaKey(PathBuf),
31    Password(String),
32}
33
34impl SshSession for LibSsh2Session {
35    type Sftp = LibSsh2Sftp;
36
37    fn connect(opts: &SshOpts) -> RemoteResult<Self> {
38        // parse configuration
39        let ssh_config = Config::try_from(opts)?;
40        // Resolve host
41        debug!("Connecting to '{}'", ssh_config.address);
42        // setup tcp stream
43        let socket_addresses: Vec<SocketAddr> = match ssh_config.address.to_socket_addrs() {
44            Ok(s) => s.collect(),
45            Err(err) => {
46                return Err(RemoteError::new_ex(
47                    RemoteErrorType::BadAddress,
48                    err.to_string(),
49                ));
50            }
51        };
52        let mut stream = None;
53        for _ in 0..ssh_config.connection_attempts {
54            for socket_addr in socket_addresses.iter() {
55                trace!(
56                    "Trying to connect to socket address '{}' (timeout: {}s)",
57                    socket_addr,
58                    ssh_config.connection_timeout.as_secs()
59                );
60                if let Ok(tcp_stream) = tcp_connect(socket_addr, ssh_config.connection_timeout) {
61                    debug!("Connection established with address {socket_addr}");
62                    stream = Some(tcp_stream);
63                    break;
64                }
65            }
66            // break from attempts cycle if some
67            if stream.is_some() {
68                break;
69            }
70        }
71        // If stream is None, return connection timeout
72        let stream = match stream {
73            Some(s) => s,
74            None => {
75                error!("No suitable socket address found; connection timeout");
76                return Err(RemoteError::new_ex(
77                    RemoteErrorType::ConnectionError,
78                    "connection timeout",
79                ));
80            }
81        };
82        // Create session
83        let mut session = match ssh2::Session::new() {
84            Ok(s) => s,
85            Err(err) => {
86                error!("Could not create session: {err}");
87                return Err(RemoteError::new_ex(RemoteErrorType::ConnectionError, err));
88            }
89        };
90        // Set TCP stream
91        session.set_tcp_stream(stream);
92        // configure algos
93        set_algo_prefs(&mut session, opts, &ssh_config)?;
94        // Open connection and initialize handshake
95        if let Err(err) = session.handshake() {
96            error!("SSH handshake failed: {err}");
97            return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
98        }
99
100        // if use_ssh_agent is enabled, try to authenticate with ssh agent
101        if let Some(ssh_agent_config) = &opts.ssh_agent_identity {
102            match session_auth_with_agent(&mut session, &ssh_config.username, ssh_agent_config) {
103                Ok(_) => {
104                    info!("Authenticated with ssh agent");
105                    return Ok(Self { session });
106                }
107                Err(err) => {
108                    error!("Could not authenticate with ssh agent: {err}");
109                }
110            }
111        }
112
113        // Authenticate with password or key
114        if !session.authenticated() {
115            let mut methods = vec![];
116            // first try with ssh agent
117            if let Some(rsa_key) = opts.key_storage.as_ref().and_then(|x| {
118                x.resolve(ssh_config.host.as_str(), ssh_config.username.as_str())
119                    .or(x.resolve(
120                        ssh_config.resolved_host.as_str(),
121                        ssh_config.username.as_str(),
122                    ))
123            }) {
124                methods.push(Authentication::RsaKey(rsa_key.clone()));
125            }
126            // then try with password
127            if let Some(password) = opts.password.as_ref() {
128                methods.push(Authentication::Password(password.clone()));
129            }
130
131            // try with methods
132            let mut last_err = None;
133            for auth_method in methods {
134                match session_auth(&mut session, opts, &ssh_config, auth_method) {
135                    Ok(_) => {
136                        info!("Authenticated successfully");
137                        return Ok(Self { session });
138                    }
139                    Err(err) => {
140                        error!("Authentication failed: {err}",);
141                        last_err = Some(err);
142                    }
143                }
144            }
145
146            return Err(match last_err {
147                Some(err) => err,
148                None => RemoteError::new_ex(
149                    RemoteErrorType::AuthenticationFailed,
150                    "no authentication method provided",
151                ),
152            });
153        }
154
155        Ok(Self { session })
156    }
157
158    fn disconnect(&self) -> RemoteResult<()> {
159        self.session
160            .disconnect(None, "Mandi!", None)
161            .map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err))
162    }
163
164    fn authenticated(&self) -> RemoteResult<bool> {
165        Ok(self.session.authenticated())
166    }
167
168    fn banner(&self) -> RemoteResult<Option<String>> {
169        Ok(self.session.banner().map(String::from))
170    }
171
172    fn cmd<S>(&mut self, cmd: S) -> RemoteResult<(u32, String)>
173    where
174        S: AsRef<str>,
175    {
176        let output = perform_shell_cmd(&mut self.session, format!("{}; echo $?", cmd.as_ref()))?;
177        if let Some(index) = output.trim().rfind('\n') {
178            trace!("Read from stdout: '{output}'");
179            let actual_output = (output[0..index + 1]).to_string();
180            trace!("Actual output '{actual_output}'");
181            trace!("Parsing return code '{}'", output[index..].trim());
182            let rc = match u32::from_str(output[index..].trim()).ok() {
183                Some(val) => val,
184                None => {
185                    return Err(RemoteError::new_ex(
186                        RemoteErrorType::ProtocolError,
187                        "Failed to get command exit code",
188                    ));
189                }
190            };
191            debug!(r#"Command output: "{actual_output}"; exit code: {rc}"#);
192            Ok((rc, actual_output))
193        } else {
194            match u32::from_str(output.trim()).ok() {
195                Some(val) => Ok((val, String::new())),
196                None => Err(RemoteError::new_ex(
197                    RemoteErrorType::ProtocolError,
198                    "Failed to get command exit code",
199                )),
200            }
201        }
202    }
203
204    fn scp_recv(&self, path: &Path) -> RemoteResult<Box<dyn Read + Send>> {
205        self.session.set_blocking(true);
206
207        self.session
208            .scp_recv(path)
209            .map(|(reader, _stat)| Box::new(reader) as Box<dyn Read + Send>)
210            .map_err(|err| {
211                RemoteError::new_ex(
212                    RemoteErrorType::ProtocolError,
213                    format!("Could not receive file over SCP: {err}"),
214                )
215            })
216    }
217
218    fn scp_send(
219        &self,
220        remote_path: &Path,
221        mode: i32,
222        size: u64,
223        times: Option<(u64, u64)>,
224    ) -> RemoteResult<Box<dyn Write + Send>> {
225        self.session.set_blocking(true);
226
227        self.session
228            .scp_send(remote_path, mode, size, times)
229            .map(|writer| Box::new(writer) as Box<dyn Write + Send>)
230            .map_err(|err| {
231                RemoteError::new_ex(
232                    RemoteErrorType::ProtocolError,
233                    format!("Could not send file over SCP: {err}"),
234                )
235            })
236    }
237
238    fn sftp(&self) -> RemoteResult<Self::Sftp> {
239        self.session.set_blocking(true);
240
241        Ok(LibSsh2Sftp {
242            inner: self.session.sftp().map_err(|err| {
243                RemoteError::new_ex(
244                    RemoteErrorType::ProtocolError,
245                    format!("Could not create SFTP session: {err}"),
246                )
247            })?,
248        })
249    }
250}
251
252struct SftpFileReader(ssh2::File);
253
254struct SftpFileWriter(ssh2::File);
255
256impl Write for SftpFileWriter {
257    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
258        self.0.write(buf)
259    }
260
261    fn flush(&mut self) -> std::io::Result<()> {
262        self.0.flush()
263    }
264}
265
266impl Seek for SftpFileWriter {
267    fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
268        self.0.seek(pos)
269    }
270}
271
272impl WriteAndSeek for SftpFileWriter {}
273
274impl Read for SftpFileReader {
275    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
276        self.0.read(buf)
277    }
278}
279
280impl Seek for SftpFileReader {
281    fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
282        self.0.seek(pos)
283    }
284}
285
286impl ReadAndSeek for SftpFileReader {}
287
288impl Sftp for LibSsh2Sftp {
289    fn mkdir(&self, path: &Path, mode: i32) -> RemoteResult<()> {
290        self.inner.mkdir(path, mode).map_err(|err| {
291            RemoteError::new_ex(
292                RemoteErrorType::FileCreateDenied,
293                format!(
294                    "Could not create directory '{path}': {err}",
295                    path = path.display()
296                ),
297            )
298        })
299    }
300
301    fn open_read(&self, path: &Path) -> RemoteResult<ReadStream> {
302        self.inner
303            .open(path)
304            .map(|file| ReadStream::from(Box::new(SftpFileReader(file)) as Box<dyn ReadAndSeek>))
305            .map_err(|err| {
306                RemoteError::new_ex(
307                    RemoteErrorType::ProtocolError,
308                    format!(
309                        "Could not open file at '{path}': {err}",
310                        path = path.display()
311                    ),
312                )
313            })
314    }
315
316    fn open_write(
317        &self,
318        path: &Path,
319        flags: super::WriteMode,
320        mode: i32,
321    ) -> RemoteResult<WriteStream> {
322        let flags = match flags {
323            super::WriteMode::Append => {
324                ssh2::OpenFlags::WRITE | ssh2::OpenFlags::APPEND | ssh2::OpenFlags::CREATE
325            }
326            super::WriteMode::Truncate => {
327                ssh2::OpenFlags::WRITE | ssh2::OpenFlags::CREATE | ssh2::OpenFlags::TRUNCATE
328            }
329        };
330
331        self.inner
332            .open_mode(path, flags, mode, OpenType::File)
333            .map(|file| WriteStream::from(Box::new(SftpFileWriter(file)) as Box<dyn WriteAndSeek>))
334            .map_err(|err| {
335                RemoteError::new_ex(
336                    RemoteErrorType::ProtocolError,
337                    format!(
338                        "Could not open file at '{path}': {err}",
339                        path = path.display()
340                    ),
341                )
342            })
343    }
344
345    fn readdir<T>(&self, dirname: T) -> RemoteResult<Vec<remotefs::File>>
346    where
347        T: AsRef<Path>,
348    {
349        self.inner
350            .readdir(dirname)
351            .map(|files| {
352                files
353                    .into_iter()
354                    .map(|(path, metadata)| self.make_fsentry(path.as_path(), &metadata))
355                    .collect()
356            })
357            .map_err(|err| {
358                RemoteError::new_ex(
359                    RemoteErrorType::ProtocolError,
360                    format!("Could not read directory: {err}",),
361                )
362            })
363    }
364
365    fn realpath(&self, path: &Path) -> RemoteResult<PathBuf> {
366        self.inner.realpath(path).map_err(|err| {
367            RemoteError::new_ex(
368                RemoteErrorType::ProtocolError,
369                format!(
370                    "Could not resolve real path for '{path}': {err}",
371                    path = path.display()
372                ),
373            )
374        })
375    }
376
377    fn rename(&self, src: &Path, dest: &Path) -> RemoteResult<()> {
378        self.inner
379            .rename(src, dest, Some(RenameFlags::OVERWRITE))
380            .map_err(|err| {
381                RemoteError::new_ex(
382                    RemoteErrorType::ProtocolError,
383                    format!("Could not rename file '{src}': {err}", src = src.display()),
384                )
385            })
386    }
387
388    fn rmdir(&self, path: &Path) -> RemoteResult<()> {
389        self.inner.rmdir(path).map_err(|err| {
390            RemoteError::new_ex(
391                RemoteErrorType::CouldNotRemoveFile,
392                format!(
393                    "Could not remove directory '{path}': {err}",
394                    path = path.display()
395                ),
396            )
397        })
398    }
399
400    fn setstat(&self, path: &Path, metadata: Metadata) -> RemoteResult<()> {
401        self.inner
402            .setstat(path, Self::metadata_to_filestat(metadata))
403            .map_err(|err| {
404                RemoteError::new_ex(
405                    RemoteErrorType::ProtocolError,
406                    format!(
407                        "Could not set file attributes for '{path}': {err}",
408                        path = path.display()
409                    ),
410                )
411            })
412    }
413
414    fn stat(&self, filename: &Path) -> RemoteResult<File> {
415        self.inner
416            .stat(filename)
417            .map(|metadata| self.make_fsentry(filename, &metadata))
418            .map_err(|err| {
419                RemoteError::new_ex(
420                    RemoteErrorType::ProtocolError,
421                    format!(
422                        "Could not get file attributes for '{filename}': {err}",
423                        filename = filename.display()
424                    ),
425                )
426            })
427    }
428
429    fn symlink(&self, path: &Path, target: &Path) -> RemoteResult<()> {
430        self.inner.symlink(path, target).map_err(|err| {
431            RemoteError::new_ex(
432                RemoteErrorType::FileCreateDenied,
433                format!(
434                    "Could not create symlink '{path}': {err}",
435                    path = path.display()
436                ),
437            )
438        })
439    }
440
441    fn unlink(&self, path: &Path) -> RemoteResult<()> {
442        self.inner.unlink(path).map_err(|err| {
443            RemoteError::new_ex(
444                RemoteErrorType::CouldNotRemoveFile,
445                format!(
446                    "Could not remove file '{path}': {err}",
447                    path = path.display()
448                ),
449            )
450        })
451    }
452}
453
454impl LibSsh2Sftp {
455    fn metadata_to_filestat(metadata: Metadata) -> FileStat {
456        let atime = metadata
457            .accessed
458            .and_then(|x| x.duration_since(SystemTime::UNIX_EPOCH).ok())
459            .map(|x| x.as_secs());
460        let mtime = metadata
461            .modified
462            .and_then(|x| x.duration_since(SystemTime::UNIX_EPOCH).ok())
463            .map(|x| x.as_secs());
464        FileStat {
465            size: Some(metadata.size),
466            uid: metadata.uid,
467            gid: metadata.gid,
468            perm: metadata.mode.map(u32::from),
469            atime,
470            mtime,
471        }
472    }
473
474    fn make_fsentry(&self, path: &Path, metadata: &FileStat) -> File {
475        let name = match path.file_name() {
476            None => "/".to_string(),
477            Some(name) => name.to_string_lossy().to_string(),
478        };
479        debug!("Found file {name}");
480        // parse metadata
481        let uid = metadata.uid;
482        let gid = metadata.gid;
483        let mode = metadata.perm.map(UnixPex::from);
484        let size = metadata.size.unwrap_or(0);
485        let accessed = metadata.atime.map(|x| {
486            SystemTime::UNIX_EPOCH
487                .checked_add(Duration::from_secs(x))
488                .unwrap_or(SystemTime::UNIX_EPOCH)
489        });
490        let modified = metadata.mtime.map(|x| {
491            SystemTime::UNIX_EPOCH
492                .checked_add(Duration::from_secs(x))
493                .unwrap_or(SystemTime::UNIX_EPOCH)
494        });
495        let symlink = match metadata.file_type().is_symlink() {
496            false => None,
497            true => match self.inner.readlink(path) {
498                Ok(p) => Some(p),
499                Err(err) => {
500                    error!(
501                        "Failed to read link of {} (even it's supposed to be a symlink): {}",
502                        path.display(),
503                        err
504                    );
505                    None
506                }
507            },
508        };
509        let file_type = if symlink.is_some() {
510            FileType::Symlink
511        } else if metadata.is_dir() {
512            FileType::Directory
513        } else {
514            FileType::File
515        };
516        let entry_metadata = Metadata {
517            accessed,
518            created: None,
519            file_type,
520            gid,
521            mode,
522            modified,
523            size,
524            symlink,
525            uid,
526        };
527        trace!("Metadata for {}: {:?}", path.display(), entry_metadata);
528        File {
529            path: path.to_path_buf(),
530            metadata: entry_metadata,
531        }
532    }
533}
534
535fn perform_shell_cmd<S: AsRef<str>>(session: &mut ssh2::Session, cmd: S) -> RemoteResult<String> {
536    // Create channel
537    trace!("Running command: {}", cmd.as_ref());
538    let mut channel = match session.channel_session() {
539        Ok(ch) => ch,
540        Err(err) => {
541            return Err(RemoteError::new_ex(
542                RemoteErrorType::ProtocolError,
543                format!("Could not open channel: {err}"),
544            ));
545        }
546    };
547
548    // escape single quotes in command
549    let cmd = cmd.as_ref().replace('\'', r#"'\''"#); // close, escape, and reopen
550
551    // Execute command; always execute inside of sh -c to have proper shell behavior.
552    // if the remote peer has fish or other non-bash shell as default, commands like
553    // "cd /some/dir; somecommand" may fail.
554    if let Err(err) = channel.exec(format!("sh -c '{cmd}'").as_str()) {
555        return Err(RemoteError::new_ex(
556            RemoteErrorType::ProtocolError,
557            format!("Could not execute command \"{cmd}\": {err}"),
558        ));
559    }
560    // Read output
561    let mut output: String = String::new();
562    match channel.read_to_string(&mut output) {
563        Ok(_) => {
564            // Wait close
565            let _ = channel.wait_close();
566            trace!("Command output: {output}");
567            Ok(output)
568        }
569        Err(err) => Err(RemoteError::new_ex(
570            RemoteErrorType::ProtocolError,
571            format!("Could not read output: {err}"),
572        )),
573    }
574}
575
576/// connect to socket address with provided timeout.
577/// If timeout is zero, don't set timeout
578fn tcp_connect(address: &SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
579    if timeout.is_zero() {
580        TcpStream::connect(address)
581    } else {
582        TcpStream::connect_timeout(address, timeout)
583    }
584}
585
586/// Configure algorithm preferences into session
587fn set_algo_prefs(
588    session: &mut ssh2::Session,
589    opts: &SshOpts,
590    config: &Config,
591) -> RemoteResult<()> {
592    // Configure preferences from config
593    let params = &config.params;
594    trace!("Configuring algorithm preferences...");
595    if let Some(compress) = params.compression {
596        trace!("compression: {compress}");
597        session.set_compress(compress);
598    }
599
600    // kex
601    let algos = params.kex_algorithms.algorithms().join(",");
602    trace!("Configuring KEX algorithms: {algos}");
603    if let Err(err) = session.method_pref(ssh2::MethodType::Kex, algos.as_str()) {
604        error!("Could not set KEX algorithms: {err}");
605        return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
606    }
607
608    // HostKey
609    let algos = params.host_key_algorithms.algorithms().join(",");
610    trace!("Configuring HostKey algorithms: {algos}");
611    if let Err(err) = session.method_pref(ssh2::MethodType::HostKey, algos.as_str()) {
612        error!("Could not set host key algorithms: {err}");
613        return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
614    }
615
616    // ciphers
617    let algos = params.ciphers.algorithms().join(",");
618    trace!("Configuring Crypt algorithms: {algos}");
619    if let Err(err) = session.method_pref(ssh2::MethodType::CryptCs, algos.as_str()) {
620        error!("Could not set crypt algorithms (client-server): {err}");
621        return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
622    }
623    if let Err(err) = session.method_pref(ssh2::MethodType::CryptSc, algos.as_str()) {
624        error!("Could not set crypt algorithms (server-client): {err}");
625        return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
626    }
627
628    // MAC
629    let algos = params.mac.algorithms().join(",");
630    trace!("Configuring MAC algorithms: {algos}");
631    if let Err(err) = session.method_pref(ssh2::MethodType::MacCs, algos.as_str()) {
632        error!("Could not set MAC algorithms (client-server): {err}");
633        return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
634    }
635    if let Err(err) = session.method_pref(ssh2::MethodType::MacSc, algos.as_str()) {
636        error!("Could not set MAC algorithms (server-client): {err}");
637        return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
638    }
639
640    // -- configure algos from opts
641    for method in opts.methods.iter() {
642        let algos = method.prefs();
643        trace!("Configuring {:?} algorithm: {}", method.method_type, algos);
644        if let Err(err) = session.method_pref(method.method_type.into(), algos.as_str()) {
645            error!("Could not set {:?} algorithms: {}", method.method_type, err);
646            return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
647        }
648    }
649    Ok(())
650}
651
652/// Authenticate on session with ssh agent
653fn session_auth_with_agent(
654    session: &mut ssh2::Session,
655    username: &str,
656    ssh_agent_config: &SshAgentIdentity,
657) -> RemoteResult<()> {
658    let mut agent = session
659        .agent()
660        .map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err))?;
661
662    agent
663        .connect()
664        .map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err))?;
665
666    agent
667        .list_identities()
668        .map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err))?;
669
670    let mut connection_result = Err(RemoteError::new(RemoteErrorType::AuthenticationFailed));
671
672    for identity in agent
673        .identities()
674        .map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err))?
675    {
676        if ssh_agent_config.pubkey_matches(identity.blob()) {
677            debug!("Trying to authenticate with ssh agent with key: {identity:?}");
678        } else {
679            continue;
680        }
681        match agent.userauth(username, &identity) {
682            Ok(()) => {
683                connection_result = Ok(());
684                debug!("Authenticated with ssh agent with key: {identity:?}");
685                break;
686            }
687            Err(err) => {
688                debug!("SSH agent auth failed: {err}");
689                connection_result = Err(RemoteError::new_ex(
690                    RemoteErrorType::AuthenticationFailed,
691                    err,
692                ));
693            }
694        }
695    }
696
697    if let Err(err) = agent.disconnect() {
698        warn!("Could not disconnect from ssh agent: {err}");
699    }
700
701    connection_result
702}
703
704/// Authenticate on session with private key
705fn session_auth_with_rsakey(
706    session: &mut ssh2::Session,
707    username: &str,
708    private_key: &Path,
709    password: Option<&str>,
710    identity_file: Option<&[PathBuf]>,
711) -> RemoteResult<()> {
712    debug!("Authenticating with username '{username}' and RSA key");
713    let mut keys = vec![private_key];
714    if let Some(identity_file) = identity_file {
715        let other_keys: Vec<&Path> = identity_file.iter().map(|x| x.as_path()).collect();
716        keys.extend(other_keys);
717    }
718    // iterate over keys
719    for key in keys.into_iter() {
720        trace!("Trying to authenticate with RSA key at '{}'", key.display());
721        match session.userauth_pubkey_file(username, None, key, password) {
722            Ok(_) => {
723                debug!("Authenticated with key at '{}'", key.display());
724                return Ok(());
725            }
726            Err(err) => {
727                error!("Authentication failed: {err}");
728            }
729        }
730    }
731    Err(RemoteError::new_ex(
732        RemoteErrorType::AuthenticationFailed,
733        "could not find any suitable RSA key to authenticate with",
734    ))
735}
736
737/// Authenticate on session with the provided [`Authentication`] method.
738fn session_auth(
739    session: &mut ssh2::Session,
740    opts: &SshOpts,
741    ssh_config: &Config,
742    authentication: Authentication,
743) -> RemoteResult<()> {
744    match authentication {
745        Authentication::RsaKey(private_key) => session_auth_with_rsakey(
746            session,
747            &ssh_config.username,
748            private_key.as_path(),
749            opts.password.as_deref(),
750            ssh_config.params.identity_file.as_deref(),
751        ),
752        Authentication::Password(password) => {
753            session_auth_with_password(session, &ssh_config.username, &password)
754        }
755    }
756}
757
758/// Authenticate on session with username and password
759fn session_auth_with_password(
760    session: &mut ssh2::Session,
761    username: &str,
762    password: &str,
763) -> RemoteResult<()> {
764    // Username / password
765    debug!("Authenticating with username '{username}' and password");
766    if let Err(err) = session.userauth_password(username, password) {
767        error!("Authentication failed: {err}");
768        Err(RemoteError::new_ex(
769            RemoteErrorType::AuthenticationFailed,
770            err,
771        ))
772    } else {
773        Ok(())
774    }
775}
776
777#[cfg(test)]
778mod test {
779
780    use ssh2_config::ParseRule;
781
782    use super::*;
783    use crate::mock::ssh as ssh_mock;
784
785    #[test]
786    fn should_connect_to_ssh_server_auth_user_password() {
787        use crate::ssh::container::OpensshServer;
788
789        let container = OpensshServer::start();
790        let port = container.port();
791
792        crate::mock::logger();
793        let config_file = ssh_mock::create_ssh_config(port);
794        let opts = SshOpts::new("sftp")
795            .config_file(config_file.path(), ParseRule::ALLOW_UNKNOWN_FIELDS)
796            .password("password");
797
798        if let Err(err) = LibSsh2Session::connect(&opts) {
799            panic!("Could not connect to server: {err}");
800        }
801        let session = LibSsh2Session::connect(&opts).unwrap();
802        assert!(session.authenticated().unwrap());
803
804        drop(container);
805    }
806
807    #[test]
808    fn should_connect_to_ssh_server_auth_key() {
809        use crate::ssh::container::OpensshServer;
810
811        let container = OpensshServer::start();
812        let port = container.port();
813
814        crate::mock::logger();
815        let config_file = ssh_mock::create_ssh_config(port);
816        let opts = SshOpts::new("sftp")
817            .config_file(config_file.path(), ParseRule::ALLOW_UNKNOWN_FIELDS)
818            .key_storage(Box::new(ssh_mock::MockSshKeyStorage::default()));
819        let session = LibSsh2Session::connect(&opts).unwrap();
820        assert!(session.authenticated().unwrap());
821    }
822
823    #[test]
824
825    fn should_perform_shell_command_on_server() {
826        crate::mock::logger();
827        let container = crate::ssh::container::OpensshServer::start();
828        let port = container.port();
829
830        let opts = SshOpts::new("127.0.0.1")
831            .port(port)
832            .username("sftp")
833            .password("password");
834        let mut session = LibSsh2Session::connect(&opts).unwrap();
835        assert!(session.authenticated().unwrap());
836        // run commands
837        assert!(session.cmd("pwd").is_ok());
838    }
839
840    #[test]
841
842    fn should_perform_shell_command_on_server_and_return_exit_code() {
843        crate::mock::logger();
844        let container = crate::ssh::container::OpensshServer::start();
845        let port = container.port();
846
847        let opts = SshOpts::new("127.0.0.1")
848            .port(port)
849            .username("sftp")
850            .password("password");
851        let mut session = LibSsh2Session::connect(&opts).unwrap();
852        assert!(session.authenticated().unwrap());
853        // run commands
854        assert_eq!(
855            session.cmd_at("pwd", Path::new("/tmp")).ok().unwrap(),
856            (0, String::from("/tmp\n"))
857        );
858        assert_eq!(
859            session
860                .cmd_at("pippopluto", Path::new("/tmp"))
861                .ok()
862                .unwrap()
863                .0,
864            127
865        );
866    }
867
868    #[test]
869    fn should_fail_authentication() {
870        crate::mock::logger();
871        let container = crate::ssh::container::OpensshServer::start();
872        let port = container.port();
873
874        let opts = SshOpts::new("127.0.0.1")
875            .port(port)
876            .username("sftp")
877            .password("ippopotamo");
878        assert!(LibSsh2Session::connect(&opts).is_err());
879    }
880
881    #[test]
882    fn test_filetransfer_sftp_bad_server() {
883        crate::mock::logger();
884        let opts = SshOpts::new("myverybad.verybad.server")
885            .port(10022)
886            .username("sftp")
887            .password("ippopotamo");
888        assert!(LibSsh2Session::connect(&opts).is_err());
889    }
890}