async_ssh2_tokio/
client.rs

1use russh::client::KeyboardInteractiveAuthResponse;
2use russh::{
3    client::{Config, Handle, Handler, Msg},
4    Channel,
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::AsyncWriteExt;
12
13use crate::ToSocketAddrsWithHostname;
14
15/// An authentification token, currently only by password.
16///
17/// Used when creating a [`Client`] for authentification.
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub enum AuthMethod {
21    Password(String),
22    PrivateKey {
23        /// entire contents of private key file
24        key_data: String,
25        key_pass: Option<String>,
26    },
27    PrivateKeyFile {
28        key_file_path: PathBuf,
29        key_pass: Option<String>,
30    },
31    #[cfg(not(target_os = "windows"))]
32    PublicKeyFile {
33        key_file_path: PathBuf,
34    },
35    KeyboardInteractive(AuthKeyboardInteractive),
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash)]
39struct PromptResponse {
40    exact: bool,
41    prompt: String,
42    response: String,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
46#[non_exhaustive]
47pub struct AuthKeyboardInteractive {
48    /// Hnts to the server the preferred methods to be used for authentication.
49    submethods: Option<String>,
50    responses: Vec<PromptResponse>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54#[non_exhaustive]
55pub enum ServerCheckMethod {
56    NoCheck,
57    /// base64 encoded key without the type prefix or hostname suffix (type is already encoded)
58    PublicKey(String),
59    PublicKeyFile(String),
60    DefaultKnownHostsFile,
61    KnownHostsFile(String),
62}
63
64impl AuthMethod {
65    /// Convenience method to create a [`AuthMethod`] from a string literal.
66    pub fn with_password(password: &str) -> Self {
67        Self::Password(password.to_string())
68    }
69
70    pub fn with_key(key: &str, passphrase: Option<&str>) -> Self {
71        Self::PrivateKey {
72            key_data: key.to_string(),
73            key_pass: passphrase.map(str::to_string),
74        }
75    }
76
77    pub fn with_key_file<T: AsRef<Path>>(key_file_path: T, passphrase: Option<&str>) -> Self {
78        Self::PrivateKeyFile {
79            key_file_path: key_file_path.as_ref().to_path_buf(),
80            key_pass: passphrase.map(str::to_string),
81        }
82    }
83
84    #[cfg(not(target_os = "windows"))]
85    pub fn with_public_key_file<T: AsRef<Path>>(key_file_path: T) -> Self {
86        Self::PublicKeyFile {
87            key_file_path: key_file_path.as_ref().to_path_buf(),
88        }
89    }
90
91    pub const fn with_keyboard_interactive(auth: AuthKeyboardInteractive) -> Self {
92        Self::KeyboardInteractive(auth)
93    }
94}
95
96impl AuthKeyboardInteractive {
97    pub fn new() -> Self {
98        Default::default()
99    }
100
101    /// Hnts to the server the preferred methods to be used for authentication.
102    pub fn with_submethods(mut self, submethods: impl Into<String>) -> Self {
103        self.submethods = Some(submethods.into());
104        self
105    }
106
107    /// Adds a response to the list of responses for a given prompt.
108    ///
109    /// The comparison for the prompt is done using a "contains".
110    pub fn with_response(mut self, prompt: impl Into<String>, response: impl Into<String>) -> Self {
111        self.responses.push(PromptResponse {
112            exact: false,
113            prompt: prompt.into(),
114            response: response.into(),
115        });
116
117        self
118    }
119
120    /// Adds a response to the list of responses for a given exact prompt.
121    pub fn with_response_exact(
122        mut self,
123        prompt: impl Into<String>,
124        response: impl Into<String>,
125    ) -> Self {
126        self.responses.push(PromptResponse {
127            exact: true,
128            prompt: prompt.into(),
129            response: response.into(),
130        });
131
132        self
133    }
134}
135
136impl PromptResponse {
137    fn matches(&self, received_prompt: &str) -> bool {
138        if self.exact {
139            self.prompt.eq(received_prompt)
140        } else {
141            received_prompt.contains(&self.prompt)
142        }
143    }
144}
145
146impl From<AuthKeyboardInteractive> for AuthMethod {
147    fn from(value: AuthKeyboardInteractive) -> Self {
148        Self::with_keyboard_interactive(value)
149    }
150}
151
152impl ServerCheckMethod {
153    /// Convenience method to create a [`ServerCheckMethod`] from a string literal.
154    pub fn with_public_key(key: &str) -> Self {
155        Self::PublicKey(key.to_string())
156    }
157
158    /// Convenience method to create a [`ServerCheckMethod`] from a string literal.
159    pub fn with_public_key_file(key_file_name: &str) -> Self {
160        Self::PublicKeyFile(key_file_name.to_string())
161    }
162
163    /// Convenience method to create a [`ServerCheckMethod`] from a string literal.
164    pub fn with_known_hosts_file(known_hosts_file: &str) -> Self {
165        Self::KnownHostsFile(known_hosts_file.to_string())
166    }
167}
168
169/// A ssh connection to a remote server.
170///
171/// After creating a `Client` by [`connect`]ing to a remote host,
172/// use [`execute`] to send commands and receive results through the connections.
173///
174/// [`connect`]: Client::connect
175/// [`execute`]: Client::execute
176///
177/// # Examples
178///
179/// ```no_run
180/// use async_ssh2_tokio::{Client, AuthMethod, ServerCheckMethod};
181/// #[tokio::main]
182/// async fn main() -> Result<(), async_ssh2_tokio::Error> {
183///     let mut client = Client::connect(
184///         ("10.10.10.2", 22),
185///         "root",
186///         AuthMethod::with_password("root"),
187///         ServerCheckMethod::NoCheck,
188///     ).await?;
189///
190///     let result = client.execute("echo Hello SSH").await?;
191///     assert_eq!(result.stdout, "Hello SSH\n");
192///     assert_eq!(result.exit_status, 0);
193///
194///     Ok(())
195/// }
196#[derive(Clone)]
197pub struct Client {
198    connection_handle: Arc<Handle<ClientHandler>>,
199    username: String,
200    address: SocketAddr,
201}
202
203impl Client {
204    /// Open a ssh connection to a remote host.
205    ///
206    /// `addr` is an address of the remote host. Anything which implements
207    /// [`ToSocketAddrsWithHostname`] trait can be supplied for the address;
208    /// ToSocketAddrsWithHostname reimplements all of [`ToSocketAddrs`];
209    /// see this trait's documentation for concrete examples.
210    ///
211    /// If `addr` yields multiple addresses, `connect` will be attempted with
212    /// each of the addresses until a connection is successful.
213    /// Authentification is tried on the first successful connection and the whole
214    /// process aborted if this fails.
215    pub async fn connect(
216        addr: impl ToSocketAddrsWithHostname,
217        username: &str,
218        auth: AuthMethod,
219        server_check: ServerCheckMethod,
220    ) -> Result<Self, crate::Error> {
221        Self::connect_with_config(addr, username, auth, server_check, Config::default()).await
222    }
223
224    /// Same as `connect`, but with the option to specify a non default
225    /// [`russh::client::Config`].
226    pub async fn connect_with_config(
227        addr: impl ToSocketAddrsWithHostname,
228        username: &str,
229        auth: AuthMethod,
230        server_check: ServerCheckMethod,
231        config: Config,
232    ) -> Result<Self, crate::Error> {
233        let config = Arc::new(config);
234
235        // Connection code inspired from std::net::TcpStream::connect and std::net::each_addr
236        let socket_addrs = addr
237            .to_socket_addrs()
238            .map_err(crate::Error::AddressInvalid)?;
239        let mut connect_res = Err(crate::Error::AddressInvalid(io::Error::new(
240            io::ErrorKind::InvalidInput,
241            "could not resolve to any addresses",
242        )));
243        for socket_addr in socket_addrs {
244            let handler = ClientHandler {
245                hostname: addr.hostname(),
246                host: socket_addr,
247                server_check: server_check.clone(),
248            };
249            match russh::client::connect(config.clone(), socket_addr, handler).await {
250                Ok(h) => {
251                    connect_res = Ok((socket_addr, h));
252                    break;
253                }
254                Err(e) => connect_res = Err(e),
255            }
256        }
257        let (address, mut handle) = connect_res?;
258        let username = username.to_string();
259
260        Self::authenticate(&mut handle, &username, auth).await?;
261
262        Ok(Self {
263            connection_handle: Arc::new(handle),
264            username,
265            address,
266        })
267    }
268
269    /// This takes a handle and performs authentification with the given method.
270    async fn authenticate(
271        handle: &mut Handle<ClientHandler>,
272        username: &String,
273        auth: AuthMethod,
274    ) -> Result<(), crate::Error> {
275        match auth {
276            AuthMethod::Password(password) => {
277                let is_authentificated = handle.authenticate_password(username, password).await?;
278                if !is_authentificated.success() {
279                    return Err(crate::Error::PasswordWrong);
280                }
281            }
282            AuthMethod::PrivateKey { key_data, key_pass } => {
283                let cprivk = russh::keys::decode_secret_key(key_data.as_str(), key_pass.as_deref())
284                    .map_err(crate::Error::KeyInvalid)?;
285                let is_authentificated = handle
286                    .authenticate_publickey(
287                        username,
288                        russh::keys::PrivateKeyWithHashAlg::new(
289                            Arc::new(cprivk),
290                            handle.best_supported_rsa_hash().await?.flatten(),
291                        ),
292                    )
293                    .await?;
294                if !is_authentificated.success() {
295                    return Err(crate::Error::KeyAuthFailed);
296                }
297            }
298            AuthMethod::PrivateKeyFile {
299                key_file_path,
300                key_pass,
301            } => {
302                let cprivk = russh::keys::load_secret_key(key_file_path, key_pass.as_deref())
303                    .map_err(crate::Error::KeyInvalid)?;
304                let is_authentificated = handle
305                    .authenticate_publickey(
306                        username,
307                        russh::keys::PrivateKeyWithHashAlg::new(
308                            Arc::new(cprivk),
309                            handle.best_supported_rsa_hash().await?.flatten(),
310                        ),
311                    )
312                    .await?;
313                if !is_authentificated.success() {
314                    return Err(crate::Error::KeyAuthFailed);
315                }
316            }
317            #[cfg(not(target_os = "windows"))]
318            AuthMethod::PublicKeyFile { key_file_path } => {
319                let cpubk = russh::keys::load_public_key(key_file_path)
320                    .map_err(crate::Error::KeyInvalid)?;
321                let mut agent = russh::keys::agent::client::AgentClient::connect_env()
322                    .await
323                    .unwrap();
324                let mut auth_identity: Option<russh::keys::PublicKey> = None;
325                for identity in agent
326                    .request_identities()
327                    .await
328                    .map_err(crate::Error::KeyInvalid)?
329                {
330                    if identity == cpubk {
331                        auth_identity = Some(identity.clone());
332                        break;
333                    }
334                }
335
336                if auth_identity.is_none() {
337                    return Err(crate::Error::KeyAuthFailed);
338                }
339
340                let is_authentificated = handle
341                    .authenticate_publickey_with(
342                        username,
343                        cpubk,
344                        handle.best_supported_rsa_hash().await?.flatten(),
345                        &mut agent,
346                    )
347                    .await?;
348                if !is_authentificated.success() {
349                    return Err(crate::Error::KeyAuthFailed);
350                }
351            }
352            AuthMethod::KeyboardInteractive(mut kbd) => {
353                let mut res = handle
354                    .authenticate_keyboard_interactive_start(username, kbd.submethods)
355                    .await?;
356                loop {
357                    let prompts = match res {
358                        KeyboardInteractiveAuthResponse::Success => break,
359                        KeyboardInteractiveAuthResponse::Failure { .. } => {
360                            return Err(crate::Error::KeyboardInteractiveAuthFailed);
361                        }
362                        KeyboardInteractiveAuthResponse::InfoRequest { prompts, .. } => prompts,
363                    };
364
365                    let mut responses = vec![];
366                    for prompt in prompts {
367                        let Some(pos) = kbd
368                            .responses
369                            .iter()
370                            .position(|pr| pr.matches(&prompt.prompt))
371                        else {
372                            return Err(crate::Error::KeyboardInteractiveNoResponseForPrompt(
373                                prompt.prompt,
374                            ));
375                        };
376                        let pr = kbd.responses.remove(pos);
377                        responses.push(pr.response);
378                    }
379
380                    res = handle
381                        .authenticate_keyboard_interactive_respond(responses)
382                        .await?;
383                }
384            }
385        };
386        Ok(())
387    }
388
389    pub async fn get_channel(&self) -> Result<Channel<Msg>, crate::Error> {
390        self.connection_handle
391            .channel_open_session()
392            .await
393            .map_err(crate::Error::SshError)
394    }
395
396    /// Open a TCP/IP forwarding channel.
397    ///
398    /// This opens a `direct-tcpip` channel to the given target.
399    pub async fn open_direct_tcpip_channel<
400        T: ToSocketAddrsWithHostname,
401        S: Into<Option<SocketAddr>>,
402    >(
403        &self,
404        target: T,
405        src: S,
406    ) -> Result<Channel<Msg>, crate::Error> {
407        let targets = target
408            .to_socket_addrs()
409            .map_err(crate::Error::AddressInvalid)?;
410        let src = src
411            .into()
412            .map(|src| (src.ip().to_string(), src.port().into()))
413            .unwrap_or_else(|| ("127.0.0.1".to_string(), 22));
414
415        let mut connect_err = crate::Error::AddressInvalid(io::Error::new(
416            io::ErrorKind::InvalidInput,
417            "could not resolve to any addresses",
418        ));
419        for target in targets {
420            match self
421                .connection_handle
422                .channel_open_direct_tcpip(
423                    target.ip().to_string(),
424                    target.port().into(),
425                    src.0.clone(),
426                    src.1,
427                )
428                .await
429            {
430                Ok(channel) => return Ok(channel),
431                Err(err) => connect_err = crate::Error::SshError(err),
432            }
433        }
434
435        Err(connect_err)
436    }
437
438    /// Upload a file with sftp to the remote server.
439    ///
440    /// `src_file_path` is the path to the file on the local machine.
441    /// `dest_file_path` is the path to the file on the remote machine.
442    /// Some sshd_config does not enable sftp by default, so make sure it is enabled.
443    /// A config line like a `Subsystem sftp internal-sftp` or
444    /// `Subsystem sftp /usr/lib/openssh/sftp-server` is needed in the sshd_config in remote machine.
445    pub async fn upload_file<T: AsRef<Path>, U: Into<String>>(
446        &self,
447        src_file_path: T,
448        //fa993: This cannot be AsRef<Path> because of underlying lib constraints as described here
449        //https://github.com/AspectUnk/russh-sftp/issues/7#issuecomment-1738355245
450        dest_file_path: U,
451    ) -> Result<(), crate::Error> {
452        // start sftp session
453        let channel = self.get_channel().await?;
454        channel.request_subsystem(true, "sftp").await?;
455        let sftp = SftpSession::new(channel.into_stream()).await?;
456
457        // read file contents locally
458        let file_contents = tokio::fs::read(src_file_path)
459            .await
460            .map_err(crate::Error::IoError)?;
461
462        // interaction with i/o
463        let mut file = sftp
464            .open_with_flags(
465                dest_file_path,
466                OpenFlags::CREATE | OpenFlags::TRUNCATE | OpenFlags::WRITE | OpenFlags::READ,
467            )
468            .await?;
469        file.write_all(&file_contents)
470            .await
471            .map_err(crate::Error::IoError)?;
472        file.flush().await.map_err(crate::Error::IoError)?;
473        file.shutdown().await.map_err(crate::Error::IoError)?;
474
475        Ok(())
476    }
477
478    /// Execute a remote command via the ssh connection.
479    ///
480    /// Returns stdout, stderr and the exit code of the command,
481    /// packaged in a [`CommandExecutedResult`] struct.
482    /// If you need the stderr output interleaved within stdout, you should postfix the command with a redirection,
483    /// e.g. `echo foo 2>&1`.
484    /// If you dont want any output at all, use something like `echo foo >/dev/null 2>&1`.
485    ///
486    /// Make sure your commands don't read from stdin and exit after bounded time.
487    ///
488    /// Can be called multiple times, but every invocation is a new shell context.
489    /// Thus `cd`, setting variables and alike have no effect on future invocations.
490    pub async fn execute(&self, command: &str) -> Result<CommandExecutedResult, crate::Error> {
491        let mut stdout_buffer = vec![];
492        let mut stderr_buffer = vec![];
493        let mut channel = self.connection_handle.channel_open_session().await?;
494        channel.exec(true, command).await?;
495
496        let mut result: Option<u32> = None;
497
498        // While the channel has messages...
499        while let Some(msg) = channel.wait().await {
500            //dbg!(&msg);
501            match msg {
502                // If we get data, add it to the buffer
503                russh::ChannelMsg::Data { ref data } => {
504                    stdout_buffer.write_all(data).await.unwrap()
505                }
506                russh::ChannelMsg::ExtendedData { ref data, ext } => {
507                    if ext == 1 {
508                        stderr_buffer.write_all(data).await.unwrap()
509                    }
510                }
511
512                // If we get an exit code report, store it, but crucially don't
513                // assume this message means end of communications. The data might
514                // not be finished yet!
515                russh::ChannelMsg::ExitStatus { exit_status } => result = Some(exit_status),
516
517                // We SHOULD get this EOF messagge, but 4254 sec 5.3 also permits
518                // the channel to close without it being sent. And sometimes this
519                // message can even precede the Data message, so don't handle it
520                // russh::ChannelMsg::Eof => break,
521                _ => {}
522            }
523        }
524
525        // If we received an exit code, report it back
526        if let Some(result) = result {
527            Ok(CommandExecutedResult {
528                stdout: String::from_utf8_lossy(&stdout_buffer).to_string(),
529                stderr: String::from_utf8_lossy(&stderr_buffer).to_string(),
530                exit_status: result,
531            })
532
533        // Otherwise, report an error
534        } else {
535            Err(crate::Error::CommandDidntExit)
536        }
537    }
538
539    /// A debugging function to get the username this client is connected as.
540    pub fn get_connection_username(&self) -> &String {
541        &self.username
542    }
543
544    /// A debugging function to get the address this client is connected to.
545    pub fn get_connection_address(&self) -> &SocketAddr {
546        &self.address
547    }
548
549    pub async fn disconnect(&self) -> Result<(), crate::Error> {
550        self.connection_handle
551            .disconnect(russh::Disconnect::ByApplication, "", "")
552            .await
553            .map_err(crate::Error::SshError)
554    }
555
556    pub fn is_closed(&self) -> bool {
557        self.connection_handle.is_closed()
558    }
559}
560
561impl Debug for Client {
562    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
563        f.debug_struct("Client")
564            .field("username", &self.username)
565            .field("address", &self.address)
566            .field("connection_handle", &"Handle<ClientHandler>")
567            .finish()
568    }
569}
570
571#[derive(Debug, Clone, PartialEq, Eq, Hash)]
572pub struct CommandExecutedResult {
573    /// The stdout output of the command.
574    pub stdout: String,
575    /// The stderr output of the command.
576    pub stderr: String,
577    /// The unix exit status (`$?` in bash).
578    pub exit_status: u32,
579}
580
581#[derive(Debug, Clone)]
582struct ClientHandler {
583    hostname: String,
584    host: SocketAddr,
585    server_check: ServerCheckMethod,
586}
587
588impl Handler for ClientHandler {
589    type Error = crate::Error;
590
591    async fn check_server_key(
592        &mut self,
593        server_public_key: &russh::keys::PublicKey,
594    ) -> Result<bool, Self::Error> {
595        match &self.server_check {
596            ServerCheckMethod::NoCheck => Ok(true),
597            ServerCheckMethod::PublicKey(key) => {
598                let pk = russh::keys::parse_public_key_base64(key)
599                    .map_err(|_| crate::Error::ServerCheckFailed)?;
600
601                Ok(pk == *server_public_key)
602            }
603            ServerCheckMethod::PublicKeyFile(key_file_name) => {
604                let pk = russh::keys::load_public_key(key_file_name)
605                    .map_err(|_| crate::Error::ServerCheckFailed)?;
606
607                Ok(pk == *server_public_key)
608            }
609            ServerCheckMethod::KnownHostsFile(known_hosts_path) => {
610                let result = russh::keys::check_known_hosts_path(
611                    &self.hostname,
612                    self.host.port(),
613                    server_public_key,
614                    known_hosts_path,
615                )
616                .map_err(|_| crate::Error::ServerCheckFailed)?;
617
618                Ok(result)
619            }
620            ServerCheckMethod::DefaultKnownHostsFile => {
621                let result = russh::keys::check_known_hosts(
622                    &self.hostname,
623                    self.host.port(),
624                    server_public_key,
625                )
626                .map_err(|_| crate::Error::ServerCheckFailed)?;
627
628                Ok(result)
629            }
630        }
631    }
632}
633
634#[cfg(test)]
635mod tests {
636    use crate::client::*;
637    use core::time;
638    use dotenv::dotenv;
639    use std::path::Path;
640    use std::sync::Once;
641    use tokio::io::AsyncReadExt;
642    static INIT: Once = Once::new();
643
644    fn initialize() {
645        // Perform your initialization tasks here
646        println!("Running initialization code before tests...");
647        // Example: load .env file if we are using non-docker environment
648        if is_running_in_docker() {
649            println!("Running inside Docker.");
650        } else {
651            println!("Not running inside Docker. Load env from file");
652            dotenv().ok();
653        }
654    }
655    fn is_running_in_docker() -> bool {
656        Path::new("/.dockerenv").exists() || check_cgroup()
657    }
658
659    fn check_cgroup() -> bool {
660        match std::fs::read_to_string("/proc/1/cgroup") {
661            Ok(contents) => contents.contains("docker"),
662            Err(_) => false,
663        }
664    }
665
666    fn env(name: &str) -> String {
667        INIT.call_once(|| {
668            initialize();
669        });
670        std::env::var(name).expect(
671            format!(
672                "Failed to get env var needed for test, make sure to set the following env var: {}",
673                name
674            )
675            .as_str(),
676        )
677    }
678
679    fn test_address() -> SocketAddr {
680        format!(
681            "{}:{}",
682            env("ASYNC_SSH2_TEST_HOST_IP"),
683            env("ASYNC_SSH2_TEST_HOST_PORT")
684        )
685        .parse()
686        .unwrap()
687    }
688
689    fn test_hostname() -> impl ToSocketAddrsWithHostname {
690        (
691            env("ASYNC_SSH2_TEST_HOST_NAME"),
692            env("ASYNC_SSH2_TEST_HOST_PORT").parse().unwrap(),
693        )
694    }
695
696    async fn establish_test_host_connection() -> Client {
697        Client::connect(
698            (
699                env("ASYNC_SSH2_TEST_HOST_IP"),
700                env("ASYNC_SSH2_TEST_HOST_PORT").parse().unwrap(),
701            ),
702            &env("ASYNC_SSH2_TEST_HOST_USER"),
703            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
704            ServerCheckMethod::NoCheck,
705        )
706        .await
707        .expect("Connection/Authentification failed")
708    }
709
710    #[tokio::test]
711    async fn connect_with_password() {
712        let client = establish_test_host_connection().await;
713        assert_eq!(
714            &env("ASYNC_SSH2_TEST_HOST_USER"),
715            client.get_connection_username(),
716        );
717        assert_eq!(test_address(), *client.get_connection_address(),);
718    }
719
720    #[tokio::test]
721    async fn execute_command_result() {
722        let client = establish_test_host_connection().await;
723        let output = client.execute("echo test!!!").await.unwrap();
724        assert_eq!("test!!!\n", output.stdout);
725        assert_eq!("", output.stderr);
726        assert_eq!(0, output.exit_status);
727    }
728
729    #[tokio::test]
730    async fn execute_command_result_stderr() {
731        let client = establish_test_host_connection().await;
732        let output = client.execute("echo test!!! 1>&2").await.unwrap();
733        assert_eq!("", output.stdout);
734        assert_eq!("test!!!\n", output.stderr);
735        assert_eq!(0, output.exit_status);
736    }
737
738    #[tokio::test]
739    async fn unicode_output() {
740        let client = establish_test_host_connection().await;
741        let output = client.execute("echo To thḙ moon! 🚀").await.unwrap();
742        assert_eq!("To thḙ moon! 🚀\n", output.stdout);
743        assert_eq!(0, output.exit_status);
744    }
745
746    #[tokio::test]
747    async fn execute_command_status() {
748        let client = establish_test_host_connection().await;
749        let output = client.execute("exit 42").await.unwrap();
750        assert_eq!(42, output.exit_status);
751    }
752
753    #[tokio::test]
754    async fn execute_multiple_commands() {
755        let client = establish_test_host_connection().await;
756        let output = client.execute("echo test!!!").await.unwrap().stdout;
757        assert_eq!("test!!!\n", output);
758
759        let output = client.execute("echo Hello World").await.unwrap().stdout;
760        assert_eq!("Hello World\n", output);
761    }
762
763    #[tokio::test]
764    async fn direct_tcpip_channel() {
765        let client = establish_test_host_connection().await;
766        let channel = client
767            .open_direct_tcpip_channel(
768                format!(
769                    "{}:{}",
770                    env("ASYNC_SSH2_TEST_HTTP_SERVER_IP"),
771                    env("ASYNC_SSH2_TEST_HTTP_SERVER_PORT"),
772                ),
773                None,
774            )
775            .await
776            .unwrap();
777
778        let mut stream = channel.into_stream();
779        stream.write_all(b"GET / HTTP/1.0\r\n\r\n").await.unwrap();
780
781        let mut response = String::new();
782        stream.read_to_string(&mut response).await.unwrap();
783
784        let body = response.split_once("\r\n\r\n").unwrap().1;
785        assert_eq!("Hello", body);
786    }
787
788    #[tokio::test]
789    async fn stderr_redirection() {
790        let client = establish_test_host_connection().await;
791
792        let output = client.execute("echo foo >/dev/null").await.unwrap();
793        assert_eq!("", output.stdout);
794
795        let output = client.execute("echo foo >>/dev/stderr").await.unwrap();
796        assert_eq!("", output.stdout);
797
798        let output = client.execute("2>&1 echo foo >>/dev/stderr").await.unwrap();
799        assert_eq!("foo\n", output.stdout);
800    }
801
802    #[tokio::test]
803    async fn sequential_commands() {
804        let client = establish_test_host_connection().await;
805
806        for i in 0..100 {
807            std::thread::sleep(time::Duration::from_millis(100));
808            let res = client
809                .execute(&format!("echo {i}"))
810                .await
811                .unwrap_or_else(|_| panic!("Execution failed in iteration {i}"));
812            assert_eq!(format!("{i}\n"), res.stdout);
813        }
814    }
815
816    #[tokio::test]
817    async fn execute_multiple_context() {
818        // This is maybe not expected behaviour, thus documenting this via a test is important.
819        let client = establish_test_host_connection().await;
820        let output = client
821            .execute("export VARIABLE=42; echo $VARIABLE")
822            .await
823            .unwrap()
824            .stdout;
825        assert_eq!("42\n", output);
826
827        let output = client.execute("echo $VARIABLE").await.unwrap().stdout;
828        assert_eq!("\n", output);
829    }
830
831    #[tokio::test]
832    async fn connect_second_address() {
833        let client = Client::connect(
834            &[SocketAddr::from(([127, 0, 0, 1], 23)), test_address()][..],
835            &env("ASYNC_SSH2_TEST_HOST_USER"),
836            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
837            ServerCheckMethod::NoCheck,
838        )
839        .await
840        .expect("Resolution to second address failed");
841
842        assert_eq!(test_address(), *client.get_connection_address(),);
843    }
844
845    #[tokio::test]
846    async fn connect_with_wrong_password() {
847        let error = Client::connect(
848            test_address(),
849            &env("ASYNC_SSH2_TEST_HOST_USER"),
850            AuthMethod::with_password("hopefully the wrong password"),
851            ServerCheckMethod::NoCheck,
852        )
853        .await
854        .expect_err("Client connected with wrong password");
855
856        match error {
857            crate::Error::PasswordWrong => {}
858            _ => panic!("Wrong error type"),
859        }
860    }
861
862    #[tokio::test]
863    async fn invalid_address() {
864        let no_client = Client::connect(
865            "this is definitely not an address",
866            &env("ASYNC_SSH2_TEST_HOST_USER"),
867            AuthMethod::with_password("hopefully the wrong password"),
868            ServerCheckMethod::NoCheck,
869        )
870        .await;
871        assert!(no_client.is_err());
872    }
873
874    #[tokio::test]
875    async fn connect_to_wrong_port() {
876        let no_client = Client::connect(
877            (env("ASYNC_SSH2_TEST_HOST_IP"), 23),
878            &env("ASYNC_SSH2_TEST_HOST_USER"),
879            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
880            ServerCheckMethod::NoCheck,
881        )
882        .await;
883        assert!(no_client.is_err());
884    }
885
886    #[tokio::test]
887    #[ignore = "This times out only after 20 seconds"]
888    async fn connect_to_wrong_host() {
889        let no_client = Client::connect(
890            "172.16.0.6:22",
891            "xxx",
892            AuthMethod::with_password("xxx"),
893            ServerCheckMethod::NoCheck,
894        )
895        .await;
896        assert!(no_client.is_err());
897    }
898
899    #[tokio::test]
900    async fn auth_key_file() {
901        let client = Client::connect(
902            test_address(),
903            &env("ASYNC_SSH2_TEST_HOST_USER"),
904            AuthMethod::with_key_file(&env("ASYNC_SSH2_TEST_CLIENT_PRIV"), None),
905            ServerCheckMethod::NoCheck,
906        )
907        .await;
908        assert!(client.is_ok());
909    }
910
911    #[tokio::test]
912    async fn auth_key_file_with_passphrase() {
913        let client = Client::connect(
914            test_address(),
915            &env("ASYNC_SSH2_TEST_HOST_USER"),
916            AuthMethod::with_key_file(
917                &env("ASYNC_SSH2_TEST_CLIENT_PROT_PRIV"),
918                Some(&env("ASYNC_SSH2_TEST_CLIENT_PROT_PASS")),
919            ),
920            ServerCheckMethod::NoCheck,
921        )
922        .await;
923        if client.is_err() {
924            println!("{:?}", client.err());
925            panic!();
926        }
927        assert!(client.is_ok());
928    }
929
930    #[tokio::test]
931    async fn auth_key_str() {
932        let key = std::fs::read_to_string(env("ASYNC_SSH2_TEST_CLIENT_PRIV")).unwrap();
933
934        let client = Client::connect(
935            test_address(),
936            &env("ASYNC_SSH2_TEST_HOST_USER"),
937            AuthMethod::with_key(key.as_str(), None),
938            ServerCheckMethod::NoCheck,
939        )
940        .await;
941        assert!(client.is_ok());
942    }
943
944    #[tokio::test]
945    async fn auth_key_str_with_passphrase() {
946        let key = std::fs::read_to_string(env("ASYNC_SSH2_TEST_CLIENT_PROT_PRIV")).unwrap();
947
948        let client = Client::connect(
949            test_address(),
950            &env("ASYNC_SSH2_TEST_HOST_USER"),
951            AuthMethod::with_key(key.as_str(), Some(&env("ASYNC_SSH2_TEST_CLIENT_PROT_PASS"))),
952            ServerCheckMethod::NoCheck,
953        )
954        .await;
955        assert!(client.is_ok());
956    }
957
958    #[tokio::test]
959    async fn auth_keyboard_interactive() {
960        let client = Client::connect(
961            test_address(),
962            &env("ASYNC_SSH2_TEST_HOST_USER"),
963            AuthKeyboardInteractive::new()
964                .with_response("Password", env("ASYNC_SSH2_TEST_HOST_PW"))
965                .into(),
966            ServerCheckMethod::NoCheck,
967        )
968        .await;
969        assert!(client.is_ok());
970    }
971
972    #[tokio::test]
973    async fn auth_keyboard_interactive_exact() {
974        let client = Client::connect(
975            test_address(),
976            &env("ASYNC_SSH2_TEST_HOST_USER"),
977            AuthKeyboardInteractive::new()
978                .with_response_exact("Password: ", env("ASYNC_SSH2_TEST_HOST_PW"))
979                .into(),
980            ServerCheckMethod::NoCheck,
981        )
982        .await;
983        assert!(client.is_ok());
984    }
985
986    #[tokio::test]
987    async fn auth_keyboard_interactive_wrong_response() {
988        let client = Client::connect(
989            test_address(),
990            &env("ASYNC_SSH2_TEST_HOST_USER"),
991            AuthKeyboardInteractive::new()
992                .with_response_exact("Password: ", "wrong password")
993                .into(),
994            ServerCheckMethod::NoCheck,
995        )
996        .await;
997        match client {
998            Err(crate::error::Error::KeyboardInteractiveAuthFailed) => {}
999            Err(e) => {
1000                panic!("Expected KeyboardInteractiveAuthFailed error. Got error: {e:?}")
1001            }
1002            Ok(_) => panic!("Expected KeyboardInteractiveAuthFailed error."),
1003        }
1004    }
1005
1006    #[tokio::test]
1007    async fn auth_keyboard_interactive_no_response() {
1008        let client = Client::connect(
1009            test_address(),
1010            &env("ASYNC_SSH2_TEST_HOST_USER"),
1011            AuthKeyboardInteractive::new()
1012                .with_response_exact("Password:", "123")
1013                .into(),
1014            ServerCheckMethod::NoCheck,
1015        )
1016        .await;
1017        match client {
1018            Err(crate::error::Error::KeyboardInteractiveNoResponseForPrompt(prompt)) => {
1019                assert_eq!(prompt, "Password: ");
1020            }
1021            Err(e) => {
1022                panic!("Expected KeyboardInteractiveNoResponseForPrompt error. Got error: {e:?}")
1023            }
1024            Ok(_) => panic!("Expected KeyboardInteractiveNoResponseForPrompt error."),
1025        }
1026    }
1027
1028    #[tokio::test]
1029    async fn server_check_file() {
1030        let client = Client::connect(
1031            test_address(),
1032            &env("ASYNC_SSH2_TEST_HOST_USER"),
1033            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1034            ServerCheckMethod::with_public_key_file(&env("ASYNC_SSH2_TEST_SERVER_PUB")),
1035        )
1036        .await;
1037        assert!(client.is_ok());
1038    }
1039
1040    #[tokio::test]
1041    async fn server_check_str() {
1042        let line = std::fs::read_to_string(env("ASYNC_SSH2_TEST_SERVER_PUB")).unwrap();
1043        let mut split = line.split_whitespace();
1044        let key = match (split.next(), split.next()) {
1045            (Some(_), Some(k)) => k,
1046            (Some(k), None) => k,
1047            _ => panic!("Failed to parse pub key file"),
1048        };
1049
1050        let client = Client::connect(
1051            test_address(),
1052            &env("ASYNC_SSH2_TEST_HOST_USER"),
1053            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1054            ServerCheckMethod::with_public_key(key),
1055        )
1056        .await;
1057        assert!(client.is_ok());
1058    }
1059
1060    #[tokio::test]
1061    async fn server_check_by_known_hosts_for_ip() {
1062        let client = Client::connect(
1063            test_address(),
1064            &env("ASYNC_SSH2_TEST_HOST_USER"),
1065            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1066            ServerCheckMethod::with_known_hosts_file(&env("ASYNC_SSH2_TEST_KNOWN_HOSTS")),
1067        )
1068        .await;
1069        assert!(client.is_ok());
1070    }
1071
1072    #[tokio::test]
1073    async fn server_check_by_known_hosts_for_hostname() {
1074        let client = Client::connect(
1075            test_hostname(),
1076            &env("ASYNC_SSH2_TEST_HOST_USER"),
1077            AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1078            ServerCheckMethod::with_known_hosts_file(&env("ASYNC_SSH2_TEST_KNOWN_HOSTS")),
1079        )
1080        .await;
1081        if is_running_in_docker() {
1082            assert!(client.is_ok());
1083        } else {
1084            assert!(client.is_err()); // DNS can't find the docker hostname if the rust running without docker container
1085        }
1086    }
1087
1088    #[tokio::test]
1089    async fn client_can_be_cloned() {
1090        let client = establish_test_host_connection().await;
1091        let client2 = client.clone();
1092
1093        let result1 = client.execute("echo test clone").await.unwrap();
1094        let result2 = client2.execute("echo test clone2").await.unwrap();
1095
1096        assert_eq!(result1.stdout, "test clone\n");
1097        assert_eq!(result2.stdout, "test clone2\n");
1098    }
1099
1100    #[tokio::test]
1101    async fn client_can_upload_file() {
1102        let client = establish_test_host_connection().await;
1103        let _ = client
1104            .upload_file(&env("ASYNC_SSH2_TEST_UPLOAD_FILE"), "/tmp/uploaded")
1105            .await
1106            .unwrap();
1107        let result = client.execute("cat /tmp/uploaded").await.unwrap();
1108        assert_eq!(result.stdout, "this is a test file\n");
1109    }
1110}