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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub enum AuthMethod {
21 Password(String),
22 PrivateKey {
23 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 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 PublicKey(String),
59 PublicKeyFile(String),
60 DefaultKnownHostsFile,
61 KnownHostsFile(String),
62}
63
64impl AuthMethod {
65 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 pub fn with_submethods(mut self, submethods: impl Into<String>) -> Self {
103 self.submethods = Some(submethods.into());
104 self
105 }
106
107 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 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 pub fn with_public_key(key: &str) -> Self {
155 Self::PublicKey(key.to_string())
156 }
157
158 pub fn with_public_key_file(key_file_name: &str) -> Self {
160 Self::PublicKeyFile(key_file_name.to_string())
161 }
162
163 pub fn with_known_hosts_file(known_hosts_file: &str) -> Self {
165 Self::KnownHostsFile(known_hosts_file.to_string())
166 }
167}
168
169#[derive(Clone)]
197pub struct Client {
198 connection_handle: Arc<Handle<ClientHandler>>,
199 username: String,
200 address: SocketAddr,
201}
202
203impl Client {
204 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 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 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 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 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 pub async fn upload_file<T: AsRef<Path>, U: Into<String>>(
446 &self,
447 src_file_path: T,
448 dest_file_path: U,
451 ) -> Result<(), crate::Error> {
452 let channel = self.get_channel().await?;
454 channel.request_subsystem(true, "sftp").await?;
455 let sftp = SftpSession::new(channel.into_stream()).await?;
456
457 let file_contents = tokio::fs::read(src_file_path)
459 .await
460 .map_err(crate::Error::IoError)?;
461
462 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 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 let Some(msg) = channel.wait().await {
500 match msg {
502 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 russh::ChannelMsg::ExitStatus { exit_status } => result = Some(exit_status),
516
517 _ => {}
522 }
523 }
524
525 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 } else {
535 Err(crate::Error::CommandDidntExit)
536 }
537 }
538
539 pub fn get_connection_username(&self) -> &String {
541 &self.username
542 }
543
544 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 pub stdout: String,
575 pub stderr: String,
577 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 println!("Running initialization code before tests...");
647 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 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()); }
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}