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