Skip to main content

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