async_ssh2_tokio/
client.rs

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