Skip to main content

async_ssh2_tokio/
client.rs

1use russh::client::KeyboardInteractiveAuthResponse;
2use russh::{
3    Channel,
4    client::{Config, Handle, Handler, Msg},
5};
6use russh_sftp::{
7    client::{Config as SftpConfig, SftpSession},
8    protocol::OpenFlags,
9};
10use std::net::SocketAddr;
11use std::sync::Arc;
12use std::time::Instant;
13use std::{fmt::Debug, path::Path};
14use std::{io, path::PathBuf};
15use tokio::io::{AsyncReadExt, AsyncWriteExt};
16use tokio::sync::mpsc;
17
18use crate::ToSocketAddrsWithHostname;
19
20/// An authentification token.
21///
22/// Used when creating a [`Client`] for authentification.
23/// Supports password, private key, public key, SSH agent, and keyboard interactive authentication.
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25#[non_exhaustive]
26pub enum AuthMethod {
27    Password(String),
28    PrivateKey {
29        /// entire contents of private key file
30        key_data: String,
31        key_pass: Option<String>,
32    },
33    PrivateKeyFile {
34        key_file_path: PathBuf,
35        key_pass: Option<String>,
36    },
37    #[cfg(not(target_os = "windows"))]
38    PublicKeyFile {
39        key_file_path: PathBuf,
40    },
41    #[cfg(not(target_os = "windows"))]
42    Agent,
43    KeyboardInteractive(AuthKeyboardInteractive),
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum SteamingOutput {
48    Stdout(Vec<u8>),
49    Stderr(Vec<u8>),
50    ExitStatus(u32),
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54struct PromptResponse {
55    exact: bool,
56    prompt: String,
57    response: String,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
61#[non_exhaustive]
62pub struct AuthKeyboardInteractive {
63    /// Hnts to the server the preferred methods to be used for authentication.
64    submethods: Option<String>,
65    responses: Vec<PromptResponse>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Hash)]
69#[non_exhaustive]
70pub enum ServerCheckMethod {
71    NoCheck,
72    /// base64 encoded key without the type prefix or hostname suffix (type is already encoded)
73    PublicKey(String),
74    PublicKeyFile(String),
75    DefaultKnownHostsFile,
76    KnownHostsFile(String),
77}
78
79impl AuthMethod {
80    /// Convenience method to create a [`AuthMethod`] from a string literal.
81    pub fn with_password(password: &str) -> Self {
82        Self::Password(password.to_string())
83    }
84
85    pub fn with_key(key: &str, passphrase: Option<&str>) -> Self {
86        Self::PrivateKey {
87            key_data: key.to_string(),
88            key_pass: passphrase.map(str::to_string),
89        }
90    }
91
92    pub fn with_key_file<T: AsRef<Path>>(key_file_path: T, passphrase: Option<&str>) -> Self {
93        Self::PrivateKeyFile {
94            key_file_path: key_file_path.as_ref().to_path_buf(),
95            key_pass: passphrase.map(str::to_string),
96        }
97    }
98
99    #[cfg(not(target_os = "windows"))]
100    pub fn with_public_key_file<T: AsRef<Path>>(key_file_path: T) -> Self {
101        Self::PublicKeyFile {
102            key_file_path: key_file_path.as_ref().to_path_buf(),
103        }
104    }
105
106    /// Creates a new SSH agent authentication method.
107    ///
108    /// This will attempt to authenticate using all identities available in the SSH agent.
109    /// The SSH agent must be running and the SSH_AUTH_SOCK environment variable must be set.
110    ///
111    /// # Example
112    /// ```no_run
113    /// use async_ssh2_tokio::client::AuthMethod;
114    ///
115    /// let auth = AuthMethod::with_agent();
116    /// ```
117    ///
118    /// # Platform Support
119    /// This method is only available on Unix-like systems (Linux, macOS, etc.).
120    /// It is not available on Windows.
121    #[cfg(not(target_os = "windows"))]
122    pub fn with_agent() -> Self {
123        Self::Agent
124    }
125
126    pub const fn with_keyboard_interactive(auth: AuthKeyboardInteractive) -> Self {
127        Self::KeyboardInteractive(auth)
128    }
129}
130
131impl AuthKeyboardInteractive {
132    pub fn new() -> Self {
133        Default::default()
134    }
135
136    /// Hnts to the server the preferred methods to be used for authentication.
137    pub fn with_submethods(mut self, submethods: impl Into<String>) -> Self {
138        self.submethods = Some(submethods.into());
139        self
140    }
141
142    /// Adds a response to the list of responses for a given prompt.
143    ///
144    /// The comparison for the prompt is done using a "contains".
145    pub fn with_response(mut self, prompt: impl Into<String>, response: impl Into<String>) -> Self {
146        self.responses.push(PromptResponse {
147            exact: false,
148            prompt: prompt.into(),
149            response: response.into(),
150        });
151
152        self
153    }
154
155    /// Adds a response to the list of responses for a given exact prompt.
156    pub fn with_response_exact(
157        mut self,
158        prompt: impl Into<String>,
159        response: impl Into<String>,
160    ) -> Self {
161        self.responses.push(PromptResponse {
162            exact: true,
163            prompt: prompt.into(),
164            response: response.into(),
165        });
166
167        self
168    }
169}
170
171impl PromptResponse {
172    fn matches(&self, received_prompt: &str) -> bool {
173        if self.exact {
174            self.prompt.eq(received_prompt)
175        } else {
176            received_prompt.contains(&self.prompt)
177        }
178    }
179}
180
181impl From<AuthKeyboardInteractive> for AuthMethod {
182    fn from(value: AuthKeyboardInteractive) -> Self {
183        Self::with_keyboard_interactive(value)
184    }
185}
186
187impl ServerCheckMethod {
188    /// Convenience method to create a [`ServerCheckMethod`] from a string literal.
189    pub fn with_public_key(key: &str) -> Self {
190        Self::PublicKey(key.to_string())
191    }
192
193    /// Convenience method to create a [`ServerCheckMethod`] from a string literal.
194    pub fn with_public_key_file(key_file_name: &str) -> Self {
195        Self::PublicKeyFile(key_file_name.to_string())
196    }
197
198    /// Convenience method to create a [`ServerCheckMethod`] from a string literal.
199    pub fn with_known_hosts_file(known_hosts_file: &str) -> Self {
200        Self::KnownHostsFile(known_hosts_file.to_string())
201    }
202}
203
204/// A ssh connection to a remote server.
205///
206/// After creating a `Client` by [`connect`]ing to a remote host,
207/// use [`execute`] to send commands and receive results through the connections.
208///
209/// [`connect`]: Client::connect
210/// [`execute`]: Client::execute
211///
212/// # Examples
213///
214/// ```no_run
215/// use async_ssh2_tokio::{Client, AuthMethod, ServerCheckMethod};
216/// #[tokio::main]
217/// async fn main() -> Result<(), async_ssh2_tokio::Error> {
218///     let mut client = Client::connect(
219///         ("10.10.10.2", 22),
220///         "root",
221///         AuthMethod::with_password("root"),
222///         ServerCheckMethod::NoCheck,
223///     ).await?;
224///
225///     let result = client.execute("echo Hello SSH").await?;
226///     assert_eq!(result.stdout, "Hello SSH\n");
227///     assert_eq!(result.exit_status, 0);
228///
229///     Ok(())
230/// }
231#[derive(Clone)]
232pub struct Client {
233    connection_handle: Arc<Handle<ClientHandler>>,
234    username: String,
235    address: SocketAddr,
236}
237
238impl Client {
239    /// Open a ssh connection to a remote host.
240    ///
241    /// `addr` is an address of the remote host. Anything which implements
242    /// [`ToSocketAddrsWithHostname`] trait can be supplied for the address;
243    /// ToSocketAddrsWithHostname reimplements all of [`ToSocketAddrs`];
244    /// see this trait's documentation for concrete examples.
245    ///
246    /// If `addr` yields multiple addresses, `connect` will be attempted with
247    /// each of the addresses until a connection is successful.
248    /// Authentification is tried on the first successful connection and the whole
249    /// process aborted if this fails.
250    pub async fn connect(
251        addr: impl ToSocketAddrsWithHostname,
252        username: &str,
253        auth: AuthMethod,
254        server_check: ServerCheckMethod,
255    ) -> Result<Self, crate::Error> {
256        Self::connect_with_config(addr, username, auth, server_check, Config::default()).await
257    }
258
259    /// Same as `connect`, but with the option to specify a non default
260    /// [`russh::client::Config`].
261    pub async fn connect_with_config(
262        addr: impl ToSocketAddrsWithHostname,
263        username: &str,
264        auth: AuthMethod,
265        server_check: ServerCheckMethod,
266        config: Config,
267    ) -> Result<Self, crate::Error> {
268        let config = Arc::new(config);
269
270        // Connection code inspired from std::net::TcpStream::connect and std::net::each_addr
271        let socket_addrs = addr
272            .to_socket_addrs()
273            .map_err(crate::Error::AddressInvalid)?;
274        let mut connect_res = Err(crate::Error::AddressInvalid(io::Error::new(
275            io::ErrorKind::InvalidInput,
276            "could not resolve to any addresses",
277        )));
278        for socket_addr in socket_addrs {
279            let handler = ClientHandler {
280                hostname: addr.hostname(),
281                host: socket_addr,
282                server_check: server_check.clone(),
283            };
284            match russh::client::connect(config.clone(), socket_addr, handler).await {
285                Ok(h) => {
286                    connect_res = Ok((socket_addr, h));
287                    break;
288                }
289                Err(e) => connect_res = Err(e),
290            }
291        }
292        let (address, mut handle) = connect_res?;
293        let username = username.to_string();
294
295        Self::authenticate(&mut handle, &username, auth).await?;
296
297        Ok(Self {
298            connection_handle: Arc::new(handle),
299            username,
300            address,
301        })
302    }
303
304    /// Open a connection to a remote host via an existing connection. Can be
305    /// used for Proxy Jumping to hosts that are only accessible from a
306    /// different remote host.
307    pub async fn connect_via(
308        via: &Client,
309        addr: impl ToSocketAddrsWithHostname,
310        username: &str,
311        auth: AuthMethod,
312        server_check: ServerCheckMethod,
313    ) -> Result<Self, crate::Error> {
314        Self::connect_via_with_config(via, addr, username, auth, server_check, Config::default())
315            .await
316    }
317
318    /// Same as `connect_via`, but with the option to specify a non default
319    /// [`russh::client::Config`].
320    pub async fn connect_via_with_config(
321        via: &Client,
322        addr: impl ToSocketAddrsWithHostname,
323        username: &str,
324        auth: AuthMethod,
325        server_check: ServerCheckMethod,
326        config: Config,
327    ) -> Result<Self, crate::Error> {
328        let config = Arc::new(config);
329
330        let socket_addrs = addr
331            .to_socket_addrs()
332            .map_err(crate::Error::AddressInvalid)?;
333        let username = username.to_string();
334        let mut connect_res = Err(crate::Error::AddressInvalid(io::Error::new(
335            io::ErrorKind::InvalidInput,
336            "could not resolve to any addresses",
337        )));
338
339        for socket_addr in socket_addrs {
340            let channel = match via.open_direct_tcpip_channel(socket_addr, None).await {
341                Ok(channel) => channel,
342                Err(e) => {
343                    connect_res = Err(e);
344                    continue;
345                }
346            };
347
348            let handler = ClientHandler {
349                hostname: addr.hostname(),
350                host: socket_addr,
351                server_check: server_check.clone(),
352            };
353
354            match russh::client::connect_stream(config.clone(), channel.into_stream(), handler)
355                .await
356            {
357                Ok(mut handle) => {
358                    Self::authenticate(&mut handle, &username, auth).await?;
359
360                    return Ok(Self {
361                        connection_handle: Arc::new(handle),
362                        username,
363                        address: socket_addr,
364                    });
365                }
366                Err(e) => connect_res = Err(e),
367            }
368        }
369
370        connect_res
371    }
372
373    /// This takes a handle and performs authentification with the given method.
374    async fn authenticate(
375        handle: &mut Handle<ClientHandler>,
376        username: &String,
377        auth: AuthMethod,
378    ) -> Result<(), crate::Error> {
379        match auth {
380            AuthMethod::Password(password) => {
381                let is_authentificated = handle.authenticate_password(username, password).await?;
382                if !is_authentificated.success() {
383                    return Err(crate::Error::PasswordWrong);
384                }
385            }
386            AuthMethod::PrivateKey { key_data, key_pass } => {
387                let cprivk = russh::keys::decode_secret_key(key_data.as_str(), key_pass.as_deref())
388                    .map_err(crate::Error::KeyInvalid)?;
389                let is_authentificated = handle
390                    .authenticate_publickey(
391                        username,
392                        russh::keys::PrivateKeyWithHashAlg::new(
393                            Arc::new(cprivk),
394                            handle.best_supported_rsa_hash().await?.flatten(),
395                        ),
396                    )
397                    .await?;
398                if !is_authentificated.success() {
399                    return Err(crate::Error::KeyAuthFailed);
400                }
401            }
402            AuthMethod::PrivateKeyFile {
403                key_file_path,
404                key_pass,
405            } => {
406                let cprivk = russh::keys::load_secret_key(key_file_path, key_pass.as_deref())
407                    .map_err(crate::Error::KeyInvalid)?;
408                let is_authentificated = handle
409                    .authenticate_publickey(
410                        username,
411                        russh::keys::PrivateKeyWithHashAlg::new(
412                            Arc::new(cprivk),
413                            handle.best_supported_rsa_hash().await?.flatten(),
414                        ),
415                    )
416                    .await?;
417                if !is_authentificated.success() {
418                    return Err(crate::Error::KeyAuthFailed);
419                }
420            }
421            #[cfg(not(target_os = "windows"))]
422            AuthMethod::PublicKeyFile { key_file_path } => {
423                let cpubk = russh::keys::load_public_key(key_file_path)
424                    .map_err(crate::Error::KeyInvalid)?;
425                let mut agent = russh::keys::agent::client::AgentClient::connect_env()
426                    .await
427                    .unwrap();
428                let mut auth_identity: Option<russh::keys::PublicKey> = None;
429                for identity in agent
430                    .request_identities()
431                    .await
432                    .map_err(crate::Error::KeyInvalid)?
433                {
434                    if *identity.public_key() == cpubk {
435                        auth_identity = Some(identity.public_key().into_owned());
436                        break;
437                    }
438                }
439
440                if auth_identity.is_none() {
441                    return Err(crate::Error::KeyAuthFailed);
442                }
443
444                let is_authentificated = handle
445                    .authenticate_publickey_with(
446                        username,
447                        cpubk,
448                        handle.best_supported_rsa_hash().await?.flatten(),
449                        &mut agent,
450                    )
451                    .await?;
452                if !is_authentificated.success() {
453                    return Err(crate::Error::KeyAuthFailed);
454                }
455            }
456            #[cfg(not(target_os = "windows"))]
457            AuthMethod::Agent => {
458                let mut agent = russh::keys::agent::client::AgentClient::connect_env()
459                    .await
460                    .map_err(|_| crate::Error::AgentConnectionFailed)?;
461
462                let identities = agent
463                    .request_identities()
464                    .await
465                    .map_err(|_| crate::Error::AgentRequestIdentitiesFailed)?;
466
467                if identities.is_empty() {
468                    return Err(crate::Error::AgentNoIdentities);
469                }
470
471                let mut auth_success = false;
472                for identity in identities {
473                    let result = handle
474                        .authenticate_publickey_with(
475                            username,
476                            identity.public_key().into_owned(),
477                            handle.best_supported_rsa_hash().await?.flatten(),
478                            &mut agent,
479                        )
480                        .await;
481
482                    if let Ok(auth_result) = result
483                        && auth_result.success()
484                    {
485                        auth_success = true;
486                        break;
487                    }
488                }
489
490                if !auth_success {
491                    return Err(crate::Error::AgentAuthenticationFailed);
492                }
493            }
494            AuthMethod::KeyboardInteractive(mut kbd) => {
495                let mut res = handle
496                    .authenticate_keyboard_interactive_start(username, kbd.submethods)
497                    .await?;
498                loop {
499                    let prompts = match res {
500                        KeyboardInteractiveAuthResponse::Success => break,
501                        KeyboardInteractiveAuthResponse::Failure { .. } => {
502                            return Err(crate::Error::KeyboardInteractiveAuthFailed);
503                        }
504                        KeyboardInteractiveAuthResponse::InfoRequest { prompts, .. } => prompts,
505                    };
506
507                    let mut responses = vec![];
508                    for prompt in prompts {
509                        let Some(pos) = kbd
510                            .responses
511                            .iter()
512                            .position(|pr| pr.matches(&prompt.prompt))
513                        else {
514                            return Err(crate::Error::KeyboardInteractiveNoResponseForPrompt(
515                                prompt.prompt,
516                            ));
517                        };
518                        let pr = kbd.responses.remove(pos);
519                        responses.push(pr.response);
520                    }
521
522                    res = handle
523                        .authenticate_keyboard_interactive_respond(responses)
524                        .await?;
525                }
526            }
527        };
528        Ok(())
529    }
530
531    pub async fn get_channel(&self) -> Result<Channel<Msg>, crate::Error> {
532        self.connection_handle
533            .channel_open_session()
534            .await
535            .map_err(crate::Error::SshError)
536    }
537
538    /// Open a TCP/IP forwarding channel.
539    ///
540    /// This opens a `direct-tcpip` channel to the given target.
541    pub async fn open_direct_tcpip_channel<
542        T: ToSocketAddrsWithHostname,
543        S: Into<Option<SocketAddr>>,
544    >(
545        &self,
546        target: T,
547        src: S,
548    ) -> Result<Channel<Msg>, crate::Error> {
549        let targets = target
550            .to_socket_addrs()
551            .map_err(crate::Error::AddressInvalid)?;
552        let src = src
553            .into()
554            .map(|src| (src.ip().to_string(), src.port().into()))
555            .unwrap_or_else(|| ("127.0.0.1".to_string(), 22));
556
557        let mut connect_err = crate::Error::AddressInvalid(io::Error::new(
558            io::ErrorKind::InvalidInput,
559            "could not resolve to any addresses",
560        ));
561        for target in targets {
562            match self
563                .connection_handle
564                .channel_open_direct_tcpip(
565                    target.ip().to_string(),
566                    target.port().into(),
567                    src.0.clone(),
568                    src.1,
569                )
570                .await
571            {
572                Ok(channel) => return Ok(channel),
573                Err(err) => connect_err = crate::Error::SshError(err),
574            }
575        }
576
577        Err(connect_err)
578    }
579
580    /// Upload a file with sftp to the remote server.
581    ///
582    /// `src_file_path` is the path to the file on the local machine.
583    /// `dest_file_path` is the path to the file on the remote machine.
584    /// 'timeout_seconds' is the timeout, in seconds, for the operation, passed on to the sftp session.
585    /// If not specified it will default to the underlying value in the sftp code, which as of this writing is 10 seconds.
586    /// 'buffer_size_in_bytes' is the value this function will buffer the file through.  it defaults to 4KB.
587    /// 'show_progress' if true, logs will be emitted every 5% of the file upload, measured in bytes
588    /// Some sshd_config does not enable sftp by default, so make sure it is enabled.
589    /// A config line like a `Subsystem sftp internal-sftp` or
590    /// `Subsystem sftp /usr/lib/openssh/sftp-server` is needed in the sshd_config in remote machine.
591    pub async fn upload_file<T, U>(
592        &self,
593        src_file_path: T,
594        //fa993: This cannot be AsRef<Path> because of underlying lib constraints as described here
595        //https://github.com/AspectUnk/russh-sftp/issues/7#issuecomment-1738355245
596        dest_file_path: U,
597        timeout_seconds: Option<u64>,
598        buffer_size_in_bytes: Option<usize>,
599        show_progress: bool,
600    ) -> Result<(), crate::Error>
601    where
602        T: AsRef<Path> + std::fmt::Display,
603        U: Into<String>,
604    {
605        // start sftp session
606        let channel = self.get_channel().await?;
607        channel.request_subsystem(true, "sftp").await?;
608        let sftp = if let Some(secs) = timeout_seconds {
609            SftpSession::new_with_config(
610                channel.into_stream(),
611                SftpConfig {
612                    request_timeout_secs: secs,
613                    ..SftpConfig::default()
614                },
615            )
616            .await?
617        } else {
618            SftpSession::new(channel.into_stream()).await?
619        };
620
621        let file_size = tokio::fs::metadata(&src_file_path).await?.len();
622        // read file contents locally
623        let local_file = tokio::fs::File::open(&src_file_path)
624            .await
625            .map_err(crate::Error::IoError)?;
626        let mut local_file_buffered = tokio::io::BufReader::new(local_file);
627
628        let dest_file_path = dest_file_path.into();
629        let mut remote_file = sftp
630            .open_with_flags(
631                dest_file_path.clone(),
632                OpenFlags::CREATE | OpenFlags::TRUNCATE | OpenFlags::WRITE | OpenFlags::READ,
633            )
634            .await?;
635
636        let buffer_size_in_bytes = buffer_size_in_bytes.unwrap_or(4096);
637        let mut buffer = vec![0; buffer_size_in_bytes];
638
639        let mut total_bytes_copied = 0;
640        let mut next_progress_marker = 5.0;
641
642        let start_time = Instant::now();
643        if show_progress {
644            log::info!(
645                "Starting file upload from {src_file_path} to {dest_file_path}, total bytes to be transferred: {}",
646                file_size
647            );
648        }
649        loop {
650            let n = local_file_buffered.read(&mut buffer).await?;
651            if n == 0 {
652                break;
653            }
654            remote_file
655                .write_all(&buffer[..n])
656                .await
657                .map_err(crate::Error::IoError)?;
658            if show_progress {
659                total_bytes_copied += n as u64;
660                let progress = (total_bytes_copied as f64 / file_size as f64) * 100.0;
661                if progress >= next_progress_marker {
662                    log::info!(
663                        "Progress of upload from {src_file_path} to {dest_file_path}: {:.0}% in elapsed time: {}s",
664                        next_progress_marker,
665                        start_time.elapsed().as_secs_f64()
666                    );
667                    next_progress_marker += 5.0;
668                }
669            }
670        }
671
672        if show_progress {
673            log::info!(
674                "file upload comprising {file_size} bytes from {src_file_path} to {dest_file_path} completed successfully in {}s",
675                start_time.elapsed().as_secs_f64()
676            );
677        }
678        remote_file
679            .shutdown()
680            .await
681            .map_err(crate::Error::IoError)?;
682
683        Ok(())
684    }
685
686    /// Download a file from the remote server using sftp.
687    ///
688    /// `remote_file_path` is the path to the file on the remote machine.
689    /// `local_file_path` is the path to the file on the local machine.
690    /// Some sshd_config does not enable sftp by default, so make sure it is enabled.
691    /// A config line like a `Subsystem sftp internal-sftp` or
692    /// `Subsystem sftp /usr/lib/openssh/sftp-server` is needed in the sshd_config in remote machine.
693    pub async fn download_file<T: AsRef<Path>, U: Into<String>>(
694        &self,
695        remote_file_path: U,
696        local_file_path: T,
697    ) -> Result<(), crate::Error> {
698        // start sftp session
699        let channel = self.get_channel().await?;
700        channel.request_subsystem(true, "sftp").await?;
701        let sftp = SftpSession::new(channel.into_stream()).await?;
702
703        // open remote file for reading
704        let mut remote_file = sftp
705            .open_with_flags(remote_file_path, OpenFlags::READ)
706            .await?;
707
708        // read remote file contents
709        let mut contents = Vec::new();
710        remote_file.read_to_end(contents.as_mut()).await?;
711
712        // write contents to local file
713        let mut local_file = tokio::fs::File::create(local_file_path.as_ref())
714            .await
715            .map_err(crate::Error::IoError)?;
716
717        local_file
718            .write_all(&contents)
719            .await
720            .map_err(crate::Error::IoError)?;
721        local_file.flush().await.map_err(crate::Error::IoError)?;
722
723        Ok(())
724    }
725
726    /// Execute a remote command via the ssh connection.
727    ///
728    /// Returns stdout, stderr and the exit code of the command,
729    /// packaged in a [`CommandExecutedResult`] struct.
730    /// If you need the stderr output interleaved within stdout, you should postfix the command with a redirection,
731    /// e.g. `echo foo 2>&1`.
732    /// If you dont want any output at all, use something like `echo foo >/dev/null 2>&1`.
733    ///
734    /// Make sure your commands don't read from stdin and exit after bounded time.
735    ///
736    /// Can be called multiple times, but every invocation is a new shell context.
737    /// Thus `cd`, setting variables and alike have no effect on future invocations.
738    pub async fn execute(&self, command: &str) -> Result<CommandExecutedResult, crate::Error> {
739        let mut stdout_buffer = vec![];
740        let mut stderr_buffer = vec![];
741        let mut channel = self.connection_handle.channel_open_session().await?;
742        channel.exec(true, command).await?;
743
744        let mut result: Option<u32> = None;
745
746        // While the channel has messages...
747        while let Some(msg) = channel.wait().await {
748            //dbg!(&msg);
749            match msg {
750                // If we get data, add it to the buffer
751                russh::ChannelMsg::Data { ref data } => {
752                    stdout_buffer.write_all(data).await.unwrap()
753                }
754                russh::ChannelMsg::ExtendedData { ref data, ext } => {
755                    if ext == 1 {
756                        stderr_buffer.write_all(data).await.unwrap()
757                    }
758                }
759
760                // If we get an exit code report, store it, but crucially don't
761                // assume this message means end of communications. The data might
762                // not be finished yet!
763                russh::ChannelMsg::ExitStatus { exit_status } => result = Some(exit_status),
764
765                // We SHOULD get this EOF messagge, but 4254 sec 5.3 also permits
766                // the channel to close without it being sent. And sometimes this
767                // message can even precede the Data message, so don't handle it
768                // russh::ChannelMsg::Eof => break,
769                _ => {}
770            }
771        }
772
773        // If we received an exit code, report it back
774        if let Some(result) = result {
775            Ok(CommandExecutedResult {
776                stdout: String::from_utf8_lossy(&stdout_buffer).to_string(),
777                stderr: String::from_utf8_lossy(&stderr_buffer).to_string(),
778                exit_status: result,
779            })
780
781        // Otherwise, report an error
782        } else {
783            Err(crate::Error::CommandDidntExit)
784        }
785    }
786
787    /// Execute a remote command via the ssh connection.
788    ///
789    /// Command output is stream to the provided channel. Returns the exit code.
790    /// The channel sends `SteamingOutput` enum variants to distinguish stdout,
791    /// stderr and exit code so message arrive interleaved and in the order
792    /// they are received. See `execute` for more details.
793    ///
794    #[deprecated(
795        since = "0.11.0",
796        note = "Use execute_io with channels directly for more flexibility.\n\
797              This method will be removed or introduced breaking changes in future versions.\n\
798              At minimum, SteamingOutput will be renamed to StreamingOutput"
799    )]
800    pub async fn execute_streaming(
801        &self,
802        command: &str,
803        ch: tokio::sync::mpsc::Sender<SteamingOutput>,
804    ) -> Result<u32, crate::Error> {
805        let (stdout_tx, mut stdout_rx) = tokio::sync::mpsc::channel(1);
806        let (stderr_tx, mut stderr_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(1);
807
808        let exec_future = self.execute_io(command, stdout_tx, Some(stderr_tx), None, false, None);
809        tokio::pin!(exec_future);
810        let result = loop {
811            tokio::select! {
812                result = &mut exec_future => break result,
813                Some(stdout) = stdout_rx.recv() => {
814                    ch.send(SteamingOutput::Stdout(stdout)).await.unwrap();
815                },
816                Some(stderr) = stderr_rx.recv() => {
817                    ch.send(SteamingOutput::Stderr(stderr)).await.unwrap();
818                },
819            };
820        }?;
821        // see if any output is left in the channels
822        if let Some(stdout) = stdout_rx.recv().await {
823            ch.send(SteamingOutput::Stdout(stdout)).await.unwrap();
824        }
825        if let Some(stderr) = stderr_rx.recv().await {
826            ch.send(SteamingOutput::Stderr(stderr)).await.unwrap();
827        }
828        ch.send(SteamingOutput::ExitStatus(result)).await.unwrap();
829        Ok(result)
830    }
831
832    /// Execute a remote command via the ssh connection and perform i/o via channels.
833    ///
834    /// `execute_io` does the same as `execute`, but ties stdin and stdout/stderr to channels.
835    /// Giving a stdin channel is optional. If there is only a stdout channel, stderr will be
836    /// sent to the stdout channel. Sending an empty string to the stdin channel will send an
837    /// EOF to the remote side.
838    /// If `request_pty` is true, a pseudo terminal is requested for the session. This is
839    /// sometime necessary for example to enter a password, which is not request via stdin
840    /// but directly from the terminal. NOTE: A pty has no stderr, so stderr output is
841    /// sent to the stdout channel.
842    /// The exit code of the command is returned as a result. If the remote ssh server
843    /// does not report an exit code, a default exit code can be passed, otherwise an error
844    /// is returned.
845    ///
846    /// Example:
847    ///
848    /// ```no_run
849    /// use async_ssh2_tokio::{Client, AuthMethod, ServerCheckMethod};
850    /// use tokio::sync::mpsc;
851    ///
852    /// #[tokio::main]
853    /// async fn main() -> Result<(), async_ssh2_tokio::Error> {
854    ///     let mut client = Client::connect(
855    ///         ("10.10.10.2", 22),
856    ///         "root",
857    ///         AuthMethod::with_password("root"),
858    ///         ServerCheckMethod::NoCheck,
859    ///     ).await?;
860    ///     let mut result_stdout = vec![];
861    ///     let mut result_stderr = vec![];
862    ///
863    ///     let (stdout_tx, mut stdout_rx) = mpsc::channel(10);
864    ///     let (stderr_tx, mut stderr_rx) = mpsc::channel(10);
865    ///     let cmd = "date";
866    ///     let exec_future = client.execute_io(&cmd, stdout_tx, Some(stderr_tx), None, false, None);
867    ///     tokio::pin!(exec_future);
868    ///     let result = loop {
869    ///         tokio::select! {
870    ///             result = &mut exec_future => break result,
871    ///             Some(stdout) = stdout_rx.recv() => {
872    ///                 println!("ssh stdout: {}", String::from_utf8_lossy(&stdout));
873    ///                 result_stdout.push(stdout);
874    ///             },
875    ///             Some(stderr) = stderr_rx.recv() => {
876    ///                 println!("ssh stderr: {}", String::from_utf8_lossy(&stderr));
877    ///                 result_stderr.push(stderr);
878    ///             },
879    ///         };
880    ///     }?;
881    ///
882    ///     // see if any output is left in the channels
883    ///     if let Some(stdout) = stdout_rx.recv().await {
884    ///         println!("ssh stdout: {}", String::from_utf8_lossy(&stdout));
885    ///         result_stdout.push(stdout);
886    ///     }
887    ///     if let Some(stderr) = stderr_rx.recv().await {
888    ///         println!("ssh stderr: {}", String::from_utf8_lossy(&stderr));
889    ///         result_stderr.push(stderr);
890    ///     }
891    ///     Ok(())
892    /// }
893    /// ```
894    pub async fn execute_io(
895        &self,
896        command: &str,
897        stdout_channel: mpsc::Sender<Vec<u8>>,
898        stderr_channel: Option<mpsc::Sender<Vec<u8>>>,
899        mut stdin_channel: Option<mpsc::Receiver<Vec<u8>>>,
900        request_pty: bool,
901        default_exit_code: Option<u32>,
902    ) -> Result<u32, crate::Error> {
903        let mut channel = self.connection_handle.channel_open_session().await?;
904
905        let mut result: Option<u32> = None;
906        if request_pty {
907            channel
908                .request_pty(false, "xterm", 80_u32, 24_u32, 0, 0, &[])
909                .await?;
910        }
911
912        channel.exec(true, command).await?;
913
914        // While the channel has messages...
915        loop {
916            let recv_stdin = async {
917                if let Some(ch) = stdin_channel.as_mut() {
918                    Some(ch.recv().await)
919                } else {
920                    None
921                }
922            };
923            tokio::select! {
924                Some(input) = recv_stdin => {
925                    if let Some(input) = input {
926                        if input.is_empty() {
927                            channel.eof().await? ;
928                        } else {
929                            channel.data(&input as &[u8]).await?;
930                        }
931                    }
932                },
933                msg = channel.wait() => {
934                    //dbg!(&msg);
935                    match msg {
936                        // If we get data, add it to the buffer
937                        Some(russh::ChannelMsg::Data { ref data }) => {
938                            //dbg!("sending stdout");
939                            stdout_channel
940                                .send(data.to_vec())
941                                .await
942                                .map_err(crate::Error::ChannelSendError)?;
943                        }
944                        Some (russh::ChannelMsg::ExtendedData { ref data, ext }) => {
945                            if ext == 1 {
946                                if let Some(stderr_channel) = &stderr_channel {
947                                    //dbg!("sending stderr");
948                                    stderr_channel
949                                        .send(data.to_vec())
950                                        .await
951                                        .map_err(crate::Error::ChannelSendError)?;
952                                } else {
953                                    //dbg!("sending stderr to stdout");
954                                    stdout_channel
955                                        .send(data.to_vec())
956                                        .await
957                                        .map_err(crate::Error::ChannelSendError)?;
958                                }
959                            }
960                        }
961
962                        // If we get an exit code report, store it, but crucially don't
963                        // assume this message means end of communications. The data might
964                        // not be finished yet!
965                        Some (russh::ChannelMsg::ExitStatus { exit_status }) => result = Some(exit_status),
966
967                        // We SHOULD get this EOF messagge, but 4254 sec 5.3 also permits
968                        // the channel to close without it being sent. And sometimes this
969                        // message can even precede the Data message, so don't handle it
970                        // russh::ChannelMsg::Eof => break,
971                        Some (_) => {},
972                        None => break,
973                    }
974                }
975            }
976        }
977
978        // If we received an exit code, report it back
979        if let Some(result) = result {
980            Ok(result)
981        // If we have an default exit code, report it back
982        } else if let Some(default_exit_code) = default_exit_code {
983            Ok(default_exit_code)
984        // Otherwise, report an error
985        } else {
986            Err(crate::Error::CommandDidntExit)
987        }
988    }
989
990    /// A debugging function to get the username this client is connected as.
991    pub fn get_connection_username(&self) -> &String {
992        &self.username
993    }
994
995    /// A debugging function to get the address this client is connected to.
996    pub fn get_connection_address(&self) -> &SocketAddr {
997        &self.address
998    }
999
1000    pub async fn disconnect(&self) -> Result<(), crate::Error> {
1001        self.connection_handle
1002            .disconnect(russh::Disconnect::ByApplication, "", "")
1003            .await
1004            .map_err(crate::Error::SshError)
1005    }
1006
1007    pub fn is_closed(&self) -> bool {
1008        self.connection_handle.is_closed()
1009    }
1010}
1011
1012impl Debug for Client {
1013    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1014        f.debug_struct("Client")
1015            .field("username", &self.username)
1016            .field("address", &self.address)
1017            .field("connection_handle", &"Handle<ClientHandler>")
1018            .finish()
1019    }
1020}
1021
1022#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1023pub struct CommandExecutedResult {
1024    /// The stdout output of the command.
1025    pub stdout: String,
1026    /// The stderr output of the command.
1027    pub stderr: String,
1028    /// The unix exit status (`$?` in bash).
1029    pub exit_status: u32,
1030}
1031
1032#[derive(Debug, Clone)]
1033struct ClientHandler {
1034    hostname: String,
1035    host: SocketAddr,
1036    server_check: ServerCheckMethod,
1037}
1038
1039impl Handler for ClientHandler {
1040    type Error = crate::Error;
1041
1042    async fn check_server_key(
1043        &mut self,
1044        server_public_key: &russh::keys::PublicKey,
1045    ) -> Result<bool, Self::Error> {
1046        match &self.server_check {
1047            ServerCheckMethod::NoCheck => Ok(true),
1048            ServerCheckMethod::PublicKey(key) => {
1049                let pk = russh::keys::parse_public_key_base64(key)
1050                    .map_err(|_| crate::Error::ServerCheckFailed)?;
1051
1052                Ok(pk == *server_public_key)
1053            }
1054            ServerCheckMethod::PublicKeyFile(key_file_name) => {
1055                let pk = russh::keys::load_public_key(key_file_name)
1056                    .map_err(|_| crate::Error::ServerCheckFailed)?;
1057
1058                Ok(pk == *server_public_key)
1059            }
1060            ServerCheckMethod::KnownHostsFile(known_hosts_path) => {
1061                let result = russh::keys::check_known_hosts_path(
1062                    &self.hostname,
1063                    self.host.port(),
1064                    server_public_key,
1065                    known_hosts_path,
1066                )
1067                .map_err(|_| crate::Error::ServerCheckFailed)?;
1068
1069                Ok(result)
1070            }
1071            ServerCheckMethod::DefaultKnownHostsFile => {
1072                let result = russh::keys::check_known_hosts(
1073                    &self.hostname,
1074                    self.host.port(),
1075                    server_public_key,
1076                )
1077                .map_err(|_| crate::Error::ServerCheckFailed)?;
1078
1079                Ok(result)
1080            }
1081        }
1082    }
1083}
1084
1085#[cfg(test)]
1086mod tests {
1087    #![allow(deprecated, clippy::useless_vec)]
1088
1089    use crate::client::*;
1090    use core::time;
1091    use dotenv::dotenv;
1092    use std::path::Path;
1093    use std::sync::Once;
1094
1095    use tokio::io::AsyncReadExt;
1096    static INIT: Once = Once::new();
1097
1098    fn initialize() {
1099        // Perform your initialization tasks here
1100        println!("Running initialization code before tests...");
1101        // Example: load .env file if we are using non-docker environment
1102        if is_running_in_docker() {
1103            println!("Running inside Docker.");
1104        } else {
1105            println!("Not running inside Docker. Load env from file");
1106            dotenv().ok();
1107        }
1108    }
1109    fn is_running_in_docker() -> bool {
1110        Path::new("/.dockerenv").exists() || check_cgroup()
1111    }
1112
1113    fn check_cgroup() -> bool {
1114        match std::fs::read_to_string("/proc/1/cgroup") {
1115            Ok(contents) => contents.contains("docker"),
1116            Err(_) => false,
1117        }
1118    }
1119
1120    fn env(name: &str) -> String {
1121        INIT.call_once(|| {
1122            initialize();
1123        });
1124        std::env::var(name).unwrap_or_else(|_| {
1125            panic!(
1126                "Failed to get env var needed for test, make sure to set the following env var: {name}",
1127            )
1128        })
1129    }
1130
1131    fn test_address() -> SocketAddr {
1132        format!(
1133            "{}:{}",
1134            env("ASYNC_SSH2_TEST_HOST_IP"),
1135            env("ASYNC_SSH2_TEST_HOST_PORT")
1136        )
1137        .parse()
1138        .unwrap()
1139    }
1140
1141    fn test_hostname() -> impl ToSocketAddrsWithHostname {
1142        (
1143            env("ASYNC_SSH2_TEST_HOST_NAME"),
1144            env("ASYNC_SSH2_TEST_HOST_PORT").parse().unwrap(),
1145        )
1146    }
1147
1148    async fn establish_test_host_connection() -> Client {
1149        Client::connect(
1150            (
1151                env("ASYNC_SSH2_TEST_HOST_IP"),
1152                env("ASYNC_SSH2_TEST_HOST_PORT").parse().unwrap(),
1153            ),
1154            &env("ASYNC_SSH2_TEST_HOST_USER"),
1155            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1156            ServerCheckMethod::NoCheck,
1157        )
1158        .await
1159        .expect("Connection/Authentification failed")
1160    }
1161
1162    #[tokio::test]
1163    async fn connect_with_password() {
1164        let client = establish_test_host_connection().await;
1165        assert_eq!(
1166            &env("ASYNC_SSH2_TEST_HOST_USER"),
1167            client.get_connection_username(),
1168        );
1169        assert_eq!(test_address(), *client.get_connection_address(),);
1170    }
1171
1172    #[tokio::test]
1173    async fn execute_command_result() {
1174        let client = establish_test_host_connection().await;
1175        let output = client.execute("echo test!!!").await.unwrap();
1176        assert_eq!("test!!!\n", output.stdout);
1177        assert_eq!("", output.stderr);
1178        assert_eq!(0, output.exit_status);
1179    }
1180
1181    #[tokio::test]
1182    async fn execute_streaming_command_result() {
1183        let (tx, mut rx) = tokio::sync::mpsc::channel(10);
1184        let client = establish_test_host_connection().await;
1185        let result = client.execute_streaming("echo test!!!", tx).await.unwrap();
1186        let mut output = Vec::new();
1187        while let Some(msg) = rx.recv().await {
1188            output.push(msg);
1189        }
1190        assert_eq!(0, result);
1191        assert_eq!(
1192            &[
1193                SteamingOutput::Stdout(b"test!!!\n".to_vec()),
1194                SteamingOutput::ExitStatus(0),
1195            ],
1196            output.as_slice(),
1197        );
1198    }
1199
1200    #[tokio::test]
1201    async fn execute_command_result_stderr() {
1202        let client = establish_test_host_connection().await;
1203        let output = client.execute("echo test!!! 1>&2").await.unwrap();
1204        assert_eq!("", output.stdout);
1205        assert_eq!("test!!!\n", output.stderr);
1206        assert_eq!(0, output.exit_status);
1207    }
1208
1209    #[tokio::test]
1210    async fn execute_streaming_command_result_stderr() {
1211        let client = establish_test_host_connection().await;
1212        let (tx, mut rx) = tokio::sync::mpsc::channel(10);
1213        let result = client
1214            .execute_streaming("echo test!!! 1>&2", tx)
1215            .await
1216            .unwrap();
1217        let mut output = Vec::new();
1218        while let Some(msg) = rx.recv().await {
1219            output.push(msg);
1220        }
1221        assert_eq!(0, result);
1222        assert_eq!(
1223            &[
1224                SteamingOutput::Stderr(b"test!!!\n".to_vec()),
1225                SteamingOutput::ExitStatus(0),
1226            ],
1227            output.as_slice()
1228        );
1229    }
1230
1231    #[tokio::test]
1232    async fn unicode_output() {
1233        let client = establish_test_host_connection().await;
1234        let output = client.execute("echo To thḙ moon! 🚀").await.unwrap();
1235        assert_eq!("To thḙ moon! 🚀\n", output.stdout);
1236        assert_eq!(0, output.exit_status);
1237    }
1238
1239    #[tokio::test]
1240    async fn execute_command_status() {
1241        let client = establish_test_host_connection().await;
1242        let output = client.execute("exit 42").await.unwrap();
1243        assert_eq!(42, output.exit_status);
1244    }
1245
1246    #[tokio::test]
1247    async fn execute_streaming_command_status() {
1248        let client = establish_test_host_connection().await;
1249        let (tx, mut rx) = tokio::sync::mpsc::channel(10);
1250        let result = client.execute_streaming("exit 42", tx).await.unwrap();
1251        let mut output = Vec::new();
1252        while let Some(msg) = rx.recv().await {
1253            output.push(msg);
1254        }
1255        assert_eq!(42, result);
1256        assert_eq!(&[SteamingOutput::ExitStatus(42),], output.as_slice());
1257    }
1258
1259    #[tokio::test]
1260    async fn execute_io_command() {
1261        let client = establish_test_host_connection().await;
1262        let (stdout_tx, mut stdout_rx) = tokio::sync::mpsc::channel(10);
1263        let (stderr_tx, mut stderr_rx) = tokio::sync::mpsc::channel(10);
1264        let cmd = "echo out1; echo err1 1>&2; echo out2; echo err2 1>&2; exit 7";
1265        let exec_future = client.execute_io(cmd, stdout_tx, Some(stderr_tx), None, false, None);
1266        tokio::pin!(exec_future);
1267        let mut result: Option<u32> = None;
1268        let mut stdout_output = vec![];
1269        let mut stderr_output = vec![];
1270        loop {
1271            tokio::select! {
1272                result_inner = &mut exec_future => {
1273                    result = Some(result_inner.unwrap());
1274                },
1275                Some(stdout) = stdout_rx.recv() => {
1276                    stdout_output.push(stdout);
1277                },
1278                Some(stderr) = stderr_rx.recv() => {
1279                    stderr_output.push(stderr);
1280                },
1281            };
1282            if result.is_some() {
1283                break;
1284            }
1285        }
1286        assert_eq!(Some(7), result);
1287        assert_eq!(
1288            vec![b"out1\n".to_vec(), b"out2\n".to_vec()].concat(),
1289            stdout_output.concat()
1290        );
1291        assert_eq!(
1292            vec![b"err1\n".to_vec(), b"err2\n".to_vec()].concat(),
1293            stderr_output.concat()
1294        );
1295    }
1296
1297    #[tokio::test]
1298    async fn execute_multiple_commands() {
1299        let client = establish_test_host_connection().await;
1300        let output = client.execute("echo test!!!").await.unwrap().stdout;
1301        assert_eq!("test!!!\n", output);
1302
1303        let output = client.execute("echo Hello World").await.unwrap().stdout;
1304        assert_eq!("Hello World\n", output);
1305    }
1306
1307    #[tokio::test]
1308    async fn direct_tcpip_channel() {
1309        let client = establish_test_host_connection().await;
1310        let channel = client
1311            .open_direct_tcpip_channel(
1312                format!(
1313                    "{}:{}",
1314                    env("ASYNC_SSH2_TEST_HTTP_SERVER_IP"),
1315                    env("ASYNC_SSH2_TEST_HTTP_SERVER_PORT"),
1316                ),
1317                None,
1318            )
1319            .await
1320            .unwrap();
1321
1322        let mut stream = channel.into_stream();
1323        stream.write_all(b"GET / HTTP/1.0\r\n\r\n").await.unwrap();
1324
1325        let mut response = String::new();
1326        stream.read_to_string(&mut response).await.unwrap();
1327
1328        let body = response.split_once("\r\n\r\n").unwrap().1;
1329        assert_eq!("Hello", body);
1330    }
1331
1332    #[tokio::test]
1333    async fn connect_via_existing_client() {
1334        let client_1 = establish_test_host_connection().await;
1335        let client_2 = Client::connect_via(
1336            &client_1,
1337            format!(
1338                "{}:{}",
1339                env("ASYNC_SSH2_TEST_SSH_SERVER_2_IP"),
1340                env("ASYNC_SSH2_TEST_SSH_SERVER_2_PORT"),
1341            ),
1342            &client_1.username,
1343            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1344            ServerCheckMethod::NoCheck,
1345        )
1346        .await
1347        .unwrap();
1348
1349        // Check hostnames to ensure we've connected to the expected places
1350        assert_eq!(
1351            "ssh-server",
1352            client_1.execute("hostname").await.unwrap().stdout.trim()
1353        );
1354        assert_eq!(
1355            "ssh-server-2",
1356            client_2.execute("hostname").await.unwrap().stdout.trim()
1357        );
1358    }
1359
1360    #[tokio::test]
1361    async fn connect_via_existing_client_multiple_addresses() {
1362        let client_1 = establish_test_host_connection().await;
1363        Client::connect_via(
1364            &client_1,
1365            vec![
1366                // First address in invalid and should fail
1367                SocketAddr::from(([10, 0, 0, 0], 22)),
1368                // Should still be able to connect via the second
1369                format!(
1370                    "{}:{}",
1371                    env("ASYNC_SSH2_TEST_SSH_SERVER_2_IP"),
1372                    env("ASYNC_SSH2_TEST_SSH_SERVER_2_PORT"),
1373                )
1374                .parse()
1375                .unwrap(),
1376            ]
1377            .as_slice(),
1378            &client_1.username,
1379            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1380            ServerCheckMethod::NoCheck,
1381        )
1382        .await
1383        .unwrap();
1384    }
1385
1386    #[tokio::test]
1387    async fn stderr_redirection() {
1388        let client = establish_test_host_connection().await;
1389
1390        let output = client.execute("echo foo >/dev/null").await.unwrap();
1391        assert_eq!("", output.stdout);
1392
1393        let output = client.execute("echo foo >>/dev/stderr").await.unwrap();
1394        assert_eq!("", output.stdout);
1395
1396        let output = client.execute("2>&1 echo foo >>/dev/stderr").await.unwrap();
1397        assert_eq!("foo\n", output.stdout);
1398    }
1399
1400    #[tokio::test]
1401    async fn sequential_commands() {
1402        let client = establish_test_host_connection().await;
1403
1404        for i in 0..100 {
1405            std::thread::sleep(time::Duration::from_millis(100));
1406            let res = client
1407                .execute(&format!("echo {i}"))
1408                .await
1409                .unwrap_or_else(|_| panic!("Execution failed in iteration {i}"));
1410            assert_eq!(format!("{i}\n"), res.stdout);
1411        }
1412    }
1413
1414    #[tokio::test]
1415    async fn execute_multiple_context() {
1416        // This is maybe not expected behaviour, thus documenting this via a test is important.
1417        let client = establish_test_host_connection().await;
1418        let output = client
1419            .execute("export VARIABLE=42; echo $VARIABLE")
1420            .await
1421            .unwrap()
1422            .stdout;
1423        assert_eq!("42\n", output);
1424
1425        let output = client.execute("echo $VARIABLE").await.unwrap().stdout;
1426        assert_eq!("\n", output);
1427    }
1428
1429    #[tokio::test]
1430    async fn connect_second_address() {
1431        let client = Client::connect(
1432            &[SocketAddr::from(([127, 0, 0, 1], 23)), test_address()][..],
1433            &env("ASYNC_SSH2_TEST_HOST_USER"),
1434            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1435            ServerCheckMethod::NoCheck,
1436        )
1437        .await
1438        .expect("Resolution to second address failed");
1439
1440        assert_eq!(test_address(), *client.get_connection_address(),);
1441    }
1442
1443    #[tokio::test]
1444    async fn connect_with_wrong_password() {
1445        let error = Client::connect(
1446            test_address(),
1447            &env("ASYNC_SSH2_TEST_HOST_USER"),
1448            AuthMethod::with_password("hopefully the wrong password"),
1449            ServerCheckMethod::NoCheck,
1450        )
1451        .await
1452        .expect_err("Client connected with wrong password");
1453
1454        match error {
1455            crate::Error::PasswordWrong => {}
1456            _ => panic!("Wrong error type"),
1457        }
1458    }
1459
1460    #[tokio::test]
1461    async fn invalid_address() {
1462        let no_client = Client::connect(
1463            "this is definitely not an address",
1464            &env("ASYNC_SSH2_TEST_HOST_USER"),
1465            AuthMethod::with_password("hopefully the wrong password"),
1466            ServerCheckMethod::NoCheck,
1467        )
1468        .await;
1469        assert!(no_client.is_err());
1470    }
1471
1472    #[tokio::test]
1473    async fn connect_to_wrong_port() {
1474        let no_client = Client::connect(
1475            (env("ASYNC_SSH2_TEST_HOST_IP"), 23),
1476            &env("ASYNC_SSH2_TEST_HOST_USER"),
1477            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1478            ServerCheckMethod::NoCheck,
1479        )
1480        .await;
1481        assert!(no_client.is_err());
1482    }
1483
1484    #[tokio::test]
1485    #[ignore = "This times out only after 20 seconds"]
1486    async fn connect_to_wrong_host() {
1487        let no_client = Client::connect(
1488            "172.16.0.6:22",
1489            "xxx",
1490            AuthMethod::with_password("xxx"),
1491            ServerCheckMethod::NoCheck,
1492        )
1493        .await;
1494        assert!(no_client.is_err());
1495    }
1496
1497    #[tokio::test]
1498    async fn auth_key_file() {
1499        let client = Client::connect(
1500            test_address(),
1501            &env("ASYNC_SSH2_TEST_HOST_USER"),
1502            AuthMethod::with_key_file(env("ASYNC_SSH2_TEST_CLIENT_PRIV"), None),
1503            ServerCheckMethod::NoCheck,
1504        )
1505        .await;
1506        assert!(client.is_ok());
1507    }
1508
1509    #[tokio::test]
1510    #[cfg(not(target_os = "windows"))]
1511    async fn auth_with_agent() {
1512        // This test requires SSH agent to be running with the test key loaded
1513        // In Docker environment, the agent is always properly configured
1514        let client = Client::connect(
1515            test_address(),
1516            &env("ASYNC_SSH2_TEST_HOST_USER"),
1517            AuthMethod::with_agent(),
1518            ServerCheckMethod::NoCheck,
1519        )
1520        .await
1521        .expect("Agent authentication should succeed with correct key loaded");
1522
1523        // Verify we can execute a command
1524        let output = client.execute("echo test").await.unwrap();
1525        assert_eq!("test\n", output.stdout);
1526    }
1527
1528    #[tokio::test]
1529    #[cfg(not(target_os = "windows"))]
1530    async fn auth_with_agent_wrong_user() {
1531        // This test verifies that agent auth fails with wrong username
1532        let result = Client::connect(
1533            test_address(),
1534            "wrong_user_that_does_not_exist",
1535            AuthMethod::with_agent(),
1536            ServerCheckMethod::NoCheck,
1537        )
1538        .await;
1539
1540        // Should fail with authentication error
1541        assert!(matches!(
1542            result,
1543            Err(crate::Error::AgentAuthenticationFailed)
1544        ));
1545    }
1546
1547    #[tokio::test]
1548    #[cfg(not(target_os = "windows"))]
1549    async fn auth_with_agent_no_sock() {
1550        // Test behavior when SSH_AUTH_SOCK is not set
1551        // Temporarily unset SSH_AUTH_SOCK for this test
1552        let original_sock = std::env::var("SSH_AUTH_SOCK").ok();
1553        unsafe {
1554            std::env::remove_var("SSH_AUTH_SOCK");
1555        }
1556
1557        let result = Client::connect(
1558            test_address(),
1559            &env("ASYNC_SSH2_TEST_HOST_USER"),
1560            AuthMethod::with_agent(),
1561            ServerCheckMethod::NoCheck,
1562        )
1563        .await;
1564
1565        // Restore original SSH_AUTH_SOCK if it was set
1566        if let Some(sock) = original_sock {
1567            unsafe {
1568                std::env::set_var("SSH_AUTH_SOCK", sock);
1569            }
1570        }
1571
1572        // Should fail with connection error
1573        assert!(matches!(result, Err(crate::Error::AgentConnectionFailed)));
1574    }
1575
1576    #[tokio::test]
1577    async fn auth_key_file_with_passphrase() {
1578        let client = Client::connect(
1579            test_address(),
1580            &env("ASYNC_SSH2_TEST_HOST_USER"),
1581            AuthMethod::with_key_file(
1582                env("ASYNC_SSH2_TEST_CLIENT_PROT_PRIV"),
1583                Some(&env("ASYNC_SSH2_TEST_CLIENT_PROT_PASS")),
1584            ),
1585            ServerCheckMethod::NoCheck,
1586        )
1587        .await;
1588        if client.is_err() {
1589            println!("{:?}", client.err());
1590            panic!();
1591        }
1592        assert!(client.is_ok());
1593    }
1594
1595    #[tokio::test]
1596    async fn auth_key_str() {
1597        let key = std::fs::read_to_string(env("ASYNC_SSH2_TEST_CLIENT_PRIV")).unwrap();
1598
1599        let client = Client::connect(
1600            test_address(),
1601            &env("ASYNC_SSH2_TEST_HOST_USER"),
1602            AuthMethod::with_key(key.as_str(), None),
1603            ServerCheckMethod::NoCheck,
1604        )
1605        .await;
1606        assert!(client.is_ok());
1607    }
1608
1609    #[tokio::test]
1610    async fn auth_key_str_with_passphrase() {
1611        let key = std::fs::read_to_string(env("ASYNC_SSH2_TEST_CLIENT_PROT_PRIV")).unwrap();
1612
1613        let client = Client::connect(
1614            test_address(),
1615            &env("ASYNC_SSH2_TEST_HOST_USER"),
1616            AuthMethod::with_key(key.as_str(), Some(&env("ASYNC_SSH2_TEST_CLIENT_PROT_PASS"))),
1617            ServerCheckMethod::NoCheck,
1618        )
1619        .await;
1620        assert!(client.is_ok());
1621    }
1622
1623    #[tokio::test]
1624    async fn auth_keyboard_interactive() {
1625        let client = Client::connect(
1626            test_address(),
1627            &env("ASYNC_SSH2_TEST_HOST_USER"),
1628            AuthKeyboardInteractive::new()
1629                .with_response("Password", env("ASYNC_SSH2_TEST_HOST_PW"))
1630                .into(),
1631            ServerCheckMethod::NoCheck,
1632        )
1633        .await;
1634        assert!(client.is_ok());
1635    }
1636
1637    #[tokio::test]
1638    async fn auth_keyboard_interactive_exact() {
1639        let client = Client::connect(
1640            test_address(),
1641            &env("ASYNC_SSH2_TEST_HOST_USER"),
1642            AuthKeyboardInteractive::new()
1643                .with_response_exact("Password: ", env("ASYNC_SSH2_TEST_HOST_PW"))
1644                .into(),
1645            ServerCheckMethod::NoCheck,
1646        )
1647        .await;
1648        assert!(client.is_ok());
1649    }
1650
1651    #[tokio::test]
1652    async fn auth_keyboard_interactive_wrong_response() {
1653        let client = Client::connect(
1654            test_address(),
1655            &env("ASYNC_SSH2_TEST_HOST_USER"),
1656            AuthKeyboardInteractive::new()
1657                .with_response_exact("Password: ", "wrong password")
1658                .into(),
1659            ServerCheckMethod::NoCheck,
1660        )
1661        .await;
1662        match client {
1663            Err(crate::error::Error::KeyboardInteractiveAuthFailed) => {}
1664            Err(e) => {
1665                panic!("Expected KeyboardInteractiveAuthFailed error. Got error: {e:?}")
1666            }
1667            Ok(_) => panic!("Expected KeyboardInteractiveAuthFailed error."),
1668        }
1669    }
1670
1671    #[tokio::test]
1672    async fn auth_keyboard_interactive_no_response() {
1673        let client = Client::connect(
1674            test_address(),
1675            &env("ASYNC_SSH2_TEST_HOST_USER"),
1676            AuthKeyboardInteractive::new()
1677                .with_response_exact("Password:", "123")
1678                .into(),
1679            ServerCheckMethod::NoCheck,
1680        )
1681        .await;
1682        match client {
1683            Err(crate::error::Error::KeyboardInteractiveNoResponseForPrompt(prompt)) => {
1684                assert_eq!(prompt, "Password: ");
1685            }
1686            Err(e) => {
1687                panic!("Expected KeyboardInteractiveNoResponseForPrompt error. Got error: {e:?}")
1688            }
1689            Ok(_) => panic!("Expected KeyboardInteractiveNoResponseForPrompt error."),
1690        }
1691    }
1692
1693    #[tokio::test]
1694    async fn server_check_file() {
1695        let client = Client::connect(
1696            test_address(),
1697            &env("ASYNC_SSH2_TEST_HOST_USER"),
1698            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1699            ServerCheckMethod::with_public_key_file(&env("ASYNC_SSH2_TEST_SERVER_PUB")),
1700        )
1701        .await;
1702        assert!(client.is_ok());
1703    }
1704
1705    #[tokio::test]
1706    async fn server_check_str() {
1707        let line = std::fs::read_to_string(env("ASYNC_SSH2_TEST_SERVER_PUB")).unwrap();
1708        let mut split = line.split_whitespace();
1709        let key = match (split.next(), split.next()) {
1710            (Some(_), Some(k)) => k,
1711            (Some(k), None) => k,
1712            _ => panic!("Failed to parse pub key file"),
1713        };
1714
1715        let client = Client::connect(
1716            test_address(),
1717            &env("ASYNC_SSH2_TEST_HOST_USER"),
1718            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1719            ServerCheckMethod::with_public_key(key),
1720        )
1721        .await;
1722        assert!(client.is_ok());
1723    }
1724
1725    #[tokio::test]
1726    async fn server_check_by_known_hosts_for_ip() {
1727        let client = Client::connect(
1728            test_address(),
1729            &env("ASYNC_SSH2_TEST_HOST_USER"),
1730            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1731            ServerCheckMethod::with_known_hosts_file(&env("ASYNC_SSH2_TEST_KNOWN_HOSTS")),
1732        )
1733        .await;
1734        assert!(client.is_ok());
1735    }
1736
1737    #[tokio::test]
1738    async fn server_check_by_known_hosts_for_hostname() {
1739        let client = Client::connect(
1740            test_hostname(),
1741            &env("ASYNC_SSH2_TEST_HOST_USER"),
1742            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1743            ServerCheckMethod::with_known_hosts_file(&env("ASYNC_SSH2_TEST_KNOWN_HOSTS")),
1744        )
1745        .await;
1746        if is_running_in_docker() {
1747            assert!(client.is_ok());
1748        } else {
1749            assert!(client.is_err()); // DNS can't find the docker hostname if the rust running without docker container
1750        }
1751    }
1752
1753    #[tokio::test]
1754    async fn client_can_be_cloned() {
1755        let client = establish_test_host_connection().await;
1756        let client2 = client.clone();
1757
1758        let result1 = client.execute("echo test clone").await.unwrap();
1759        let result2 = client2.execute("echo test clone2").await.unwrap();
1760
1761        assert_eq!(result1.stdout, "test clone\n");
1762        assert_eq!(result2.stdout, "test clone2\n");
1763    }
1764
1765    #[tokio::test]
1766    async fn client_can_upload_file() {
1767        let client = establish_test_host_connection().await;
1768        client
1769            .upload_file(
1770                &env("ASYNC_SSH2_TEST_UPLOAD_FILE"),
1771                "/tmp/uploaded",
1772                None,
1773                None,
1774                false,
1775            )
1776            .await
1777            .unwrap();
1778        let result = client.execute("cat /tmp/uploaded").await.unwrap();
1779        assert_eq!(result.stdout, "this is a test file\n");
1780    }
1781
1782    #[tokio::test]
1783    async fn client_can_download_file() {
1784        let client = establish_test_host_connection().await;
1785
1786        client
1787            .execute("echo 'this is a downloaded test file' > /tmp/test_download")
1788            .await
1789            .unwrap();
1790
1791        let local_path = std::env::temp_dir().join("downloaded_test_file");
1792        client
1793            .download_file("/tmp/test_download", &local_path)
1794            .await
1795            .unwrap();
1796
1797        let contents = tokio::fs::read_to_string(&local_path).await.unwrap();
1798        assert_eq!(contents, "this is a downloaded test file\n");
1799    }
1800}