1use russh::client::KeyboardInteractiveAuthResponse;
2use russh::{
3 Channel,
4 client::{Config, Handle, Handler, Msg},
5};
6use russh_sftp::{client::SftpSession, protocol::OpenFlags};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use std::{fmt::Debug, path::Path};
10use std::{io, path::PathBuf};
11use tokio::io::{AsyncReadExt, AsyncWriteExt};
12use tokio::sync::mpsc;
13
14use crate::ToSocketAddrsWithHostname;
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21#[non_exhaustive]
22pub enum AuthMethod {
23 Password(String),
24 PrivateKey {
25 key_data: String,
27 key_pass: Option<String>,
28 },
29 PrivateKeyFile {
30 key_file_path: PathBuf,
31 key_pass: Option<String>,
32 },
33 #[cfg(not(target_os = "windows"))]
34 PublicKeyFile {
35 key_file_path: PathBuf,
36 },
37 #[cfg(not(target_os = "windows"))]
38 Agent,
39 KeyboardInteractive(AuthKeyboardInteractive),
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum SteamingOutput {
44 Stdout(Vec<u8>),
45 Stderr(Vec<u8>),
46 ExitStatus(u32),
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Hash)]
50struct PromptResponse {
51 exact: bool,
52 prompt: String,
53 response: String,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
57#[non_exhaustive]
58pub struct AuthKeyboardInteractive {
59 submethods: Option<String>,
61 responses: Vec<PromptResponse>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65#[non_exhaustive]
66pub enum ServerCheckMethod {
67 NoCheck,
68 PublicKey(String),
70 PublicKeyFile(String),
71 DefaultKnownHostsFile,
72 KnownHostsFile(String),
73}
74
75impl AuthMethod {
76 pub fn with_password(password: &str) -> Self {
78 Self::Password(password.to_string())
79 }
80
81 pub fn with_key(key: &str, passphrase: Option<&str>) -> Self {
82 Self::PrivateKey {
83 key_data: key.to_string(),
84 key_pass: passphrase.map(str::to_string),
85 }
86 }
87
88 pub fn with_key_file<T: AsRef<Path>>(key_file_path: T, passphrase: Option<&str>) -> Self {
89 Self::PrivateKeyFile {
90 key_file_path: key_file_path.as_ref().to_path_buf(),
91 key_pass: passphrase.map(str::to_string),
92 }
93 }
94
95 #[cfg(not(target_os = "windows"))]
96 pub fn with_public_key_file<T: AsRef<Path>>(key_file_path: T) -> Self {
97 Self::PublicKeyFile {
98 key_file_path: key_file_path.as_ref().to_path_buf(),
99 }
100 }
101
102 #[cfg(not(target_os = "windows"))]
118 pub fn with_agent() -> Self {
119 Self::Agent
120 }
121
122 pub const fn with_keyboard_interactive(auth: AuthKeyboardInteractive) -> Self {
123 Self::KeyboardInteractive(auth)
124 }
125}
126
127impl AuthKeyboardInteractive {
128 pub fn new() -> Self {
129 Default::default()
130 }
131
132 pub fn with_submethods(mut self, submethods: impl Into<String>) -> Self {
134 self.submethods = Some(submethods.into());
135 self
136 }
137
138 pub fn with_response(mut self, prompt: impl Into<String>, response: impl Into<String>) -> Self {
142 self.responses.push(PromptResponse {
143 exact: false,
144 prompt: prompt.into(),
145 response: response.into(),
146 });
147
148 self
149 }
150
151 pub fn with_response_exact(
153 mut self,
154 prompt: impl Into<String>,
155 response: impl Into<String>,
156 ) -> Self {
157 self.responses.push(PromptResponse {
158 exact: true,
159 prompt: prompt.into(),
160 response: response.into(),
161 });
162
163 self
164 }
165}
166
167impl PromptResponse {
168 fn matches(&self, received_prompt: &str) -> bool {
169 if self.exact {
170 self.prompt.eq(received_prompt)
171 } else {
172 received_prompt.contains(&self.prompt)
173 }
174 }
175}
176
177impl From<AuthKeyboardInteractive> for AuthMethod {
178 fn from(value: AuthKeyboardInteractive) -> Self {
179 Self::with_keyboard_interactive(value)
180 }
181}
182
183impl ServerCheckMethod {
184 pub fn with_public_key(key: &str) -> Self {
186 Self::PublicKey(key.to_string())
187 }
188
189 pub fn with_public_key_file(key_file_name: &str) -> Self {
191 Self::PublicKeyFile(key_file_name.to_string())
192 }
193
194 pub fn with_known_hosts_file(known_hosts_file: &str) -> Self {
196 Self::KnownHostsFile(known_hosts_file.to_string())
197 }
198}
199
200#[derive(Clone)]
228pub struct Client {
229 connection_handle: Arc<Handle<ClientHandler>>,
230 username: String,
231 address: SocketAddr,
232}
233
234impl Client {
235 pub async fn connect(
247 addr: impl ToSocketAddrsWithHostname,
248 username: &str,
249 auth: AuthMethod,
250 server_check: ServerCheckMethod,
251 ) -> Result<Self, crate::Error> {
252 Self::connect_with_config(addr, username, auth, server_check, Config::default()).await
253 }
254
255 pub async fn connect_with_config(
258 addr: impl ToSocketAddrsWithHostname,
259 username: &str,
260 auth: AuthMethod,
261 server_check: ServerCheckMethod,
262 config: Config,
263 ) -> Result<Self, crate::Error> {
264 let config = Arc::new(config);
265
266 let socket_addrs = addr
268 .to_socket_addrs()
269 .map_err(crate::Error::AddressInvalid)?;
270 let mut connect_res = Err(crate::Error::AddressInvalid(io::Error::new(
271 io::ErrorKind::InvalidInput,
272 "could not resolve to any addresses",
273 )));
274 for socket_addr in socket_addrs {
275 let handler = ClientHandler {
276 hostname: addr.hostname(),
277 host: socket_addr,
278 server_check: server_check.clone(),
279 };
280 match russh::client::connect(config.clone(), socket_addr, handler).await {
281 Ok(h) => {
282 connect_res = Ok((socket_addr, h));
283 break;
284 }
285 Err(e) => connect_res = Err(e),
286 }
287 }
288 let (address, mut handle) = connect_res?;
289 let username = username.to_string();
290
291 Self::authenticate(&mut handle, &username, auth).await?;
292
293 Ok(Self {
294 connection_handle: Arc::new(handle),
295 username,
296 address,
297 })
298 }
299
300 async fn authenticate(
302 handle: &mut Handle<ClientHandler>,
303 username: &String,
304 auth: AuthMethod,
305 ) -> Result<(), crate::Error> {
306 match auth {
307 AuthMethod::Password(password) => {
308 let is_authentificated = handle.authenticate_password(username, password).await?;
309 if !is_authentificated.success() {
310 return Err(crate::Error::PasswordWrong);
311 }
312 }
313 AuthMethod::PrivateKey { key_data, key_pass } => {
314 let cprivk = russh::keys::decode_secret_key(key_data.as_str(), key_pass.as_deref())
315 .map_err(crate::Error::KeyInvalid)?;
316 let is_authentificated = handle
317 .authenticate_publickey(
318 username,
319 russh::keys::PrivateKeyWithHashAlg::new(
320 Arc::new(cprivk),
321 handle.best_supported_rsa_hash().await?.flatten(),
322 ),
323 )
324 .await?;
325 if !is_authentificated.success() {
326 return Err(crate::Error::KeyAuthFailed);
327 }
328 }
329 AuthMethod::PrivateKeyFile {
330 key_file_path,
331 key_pass,
332 } => {
333 let cprivk = russh::keys::load_secret_key(key_file_path, key_pass.as_deref())
334 .map_err(crate::Error::KeyInvalid)?;
335 let is_authentificated = handle
336 .authenticate_publickey(
337 username,
338 russh::keys::PrivateKeyWithHashAlg::new(
339 Arc::new(cprivk),
340 handle.best_supported_rsa_hash().await?.flatten(),
341 ),
342 )
343 .await?;
344 if !is_authentificated.success() {
345 return Err(crate::Error::KeyAuthFailed);
346 }
347 }
348 #[cfg(not(target_os = "windows"))]
349 AuthMethod::PublicKeyFile { key_file_path } => {
350 let cpubk = russh::keys::load_public_key(key_file_path)
351 .map_err(crate::Error::KeyInvalid)?;
352 let mut agent = russh::keys::agent::client::AgentClient::connect_env()
353 .await
354 .unwrap();
355 let mut auth_identity: Option<russh::keys::PublicKey> = None;
356 for identity in agent
357 .request_identities()
358 .await
359 .map_err(crate::Error::KeyInvalid)?
360 {
361 if identity == cpubk {
362 auth_identity = Some(identity.clone());
363 break;
364 }
365 }
366
367 if auth_identity.is_none() {
368 return Err(crate::Error::KeyAuthFailed);
369 }
370
371 let is_authentificated = handle
372 .authenticate_publickey_with(
373 username,
374 cpubk,
375 handle.best_supported_rsa_hash().await?.flatten(),
376 &mut agent,
377 )
378 .await?;
379 if !is_authentificated.success() {
380 return Err(crate::Error::KeyAuthFailed);
381 }
382 }
383 #[cfg(not(target_os = "windows"))]
384 AuthMethod::Agent => {
385 let mut agent = russh::keys::agent::client::AgentClient::connect_env()
386 .await
387 .map_err(|_| crate::Error::AgentConnectionFailed)?;
388
389 let identities = agent
390 .request_identities()
391 .await
392 .map_err(|_| crate::Error::AgentRequestIdentitiesFailed)?;
393
394 if identities.is_empty() {
395 return Err(crate::Error::AgentNoIdentities);
396 }
397
398 let mut auth_success = false;
399 for identity in identities {
400 let result = handle
401 .authenticate_publickey_with(
402 username,
403 identity.clone(),
404 handle.best_supported_rsa_hash().await?.flatten(),
405 &mut agent,
406 )
407 .await;
408
409 if let Ok(auth_result) = result
410 && auth_result.success()
411 {
412 auth_success = true;
413 break;
414 }
415 }
416
417 if !auth_success {
418 return Err(crate::Error::AgentAuthenticationFailed);
419 }
420 }
421 AuthMethod::KeyboardInteractive(mut kbd) => {
422 let mut res = handle
423 .authenticate_keyboard_interactive_start(username, kbd.submethods)
424 .await?;
425 loop {
426 let prompts = match res {
427 KeyboardInteractiveAuthResponse::Success => break,
428 KeyboardInteractiveAuthResponse::Failure { .. } => {
429 return Err(crate::Error::KeyboardInteractiveAuthFailed);
430 }
431 KeyboardInteractiveAuthResponse::InfoRequest { prompts, .. } => prompts,
432 };
433
434 let mut responses = vec![];
435 for prompt in prompts {
436 let Some(pos) = kbd
437 .responses
438 .iter()
439 .position(|pr| pr.matches(&prompt.prompt))
440 else {
441 return Err(crate::Error::KeyboardInteractiveNoResponseForPrompt(
442 prompt.prompt,
443 ));
444 };
445 let pr = kbd.responses.remove(pos);
446 responses.push(pr.response);
447 }
448
449 res = handle
450 .authenticate_keyboard_interactive_respond(responses)
451 .await?;
452 }
453 }
454 };
455 Ok(())
456 }
457
458 pub async fn get_channel(&self) -> Result<Channel<Msg>, crate::Error> {
459 self.connection_handle
460 .channel_open_session()
461 .await
462 .map_err(crate::Error::SshError)
463 }
464
465 pub async fn open_direct_tcpip_channel<
469 T: ToSocketAddrsWithHostname,
470 S: Into<Option<SocketAddr>>,
471 >(
472 &self,
473 target: T,
474 src: S,
475 ) -> Result<Channel<Msg>, crate::Error> {
476 let targets = target
477 .to_socket_addrs()
478 .map_err(crate::Error::AddressInvalid)?;
479 let src = src
480 .into()
481 .map(|src| (src.ip().to_string(), src.port().into()))
482 .unwrap_or_else(|| ("127.0.0.1".to_string(), 22));
483
484 let mut connect_err = crate::Error::AddressInvalid(io::Error::new(
485 io::ErrorKind::InvalidInput,
486 "could not resolve to any addresses",
487 ));
488 for target in targets {
489 match self
490 .connection_handle
491 .channel_open_direct_tcpip(
492 target.ip().to_string(),
493 target.port().into(),
494 src.0.clone(),
495 src.1,
496 )
497 .await
498 {
499 Ok(channel) => return Ok(channel),
500 Err(err) => connect_err = crate::Error::SshError(err),
501 }
502 }
503
504 Err(connect_err)
505 }
506
507 pub async fn upload_file<T: AsRef<Path>, U: Into<String>>(
515 &self,
516 src_file_path: T,
517 dest_file_path: U,
520 ) -> Result<(), crate::Error> {
521 let channel = self.get_channel().await?;
523 channel.request_subsystem(true, "sftp").await?;
524 let sftp = SftpSession::new(channel.into_stream()).await?;
525
526 let file_contents = tokio::fs::read(src_file_path)
528 .await
529 .map_err(crate::Error::IoError)?;
530
531 let mut file = sftp
533 .open_with_flags(
534 dest_file_path,
535 OpenFlags::CREATE | OpenFlags::TRUNCATE | OpenFlags::WRITE | OpenFlags::READ,
536 )
537 .await?;
538 file.write_all(&file_contents)
539 .await
540 .map_err(crate::Error::IoError)?;
541 file.flush().await.map_err(crate::Error::IoError)?;
542 file.shutdown().await.map_err(crate::Error::IoError)?;
543
544 Ok(())
545 }
546
547 pub async fn download_file<T: AsRef<Path>, U: Into<String>>(
555 &self,
556 remote_file_path: U,
557 local_file_path: T,
558 ) -> Result<(), crate::Error> {
559 let channel = self.get_channel().await?;
561 channel.request_subsystem(true, "sftp").await?;
562 let sftp = SftpSession::new(channel.into_stream()).await?;
563
564 let mut remote_file = sftp
566 .open_with_flags(remote_file_path, OpenFlags::READ)
567 .await?;
568
569 let mut contents = Vec::new();
571 remote_file.read_to_end(contents.as_mut()).await?;
572
573 let mut local_file = tokio::fs::File::create(local_file_path.as_ref())
575 .await
576 .map_err(crate::Error::IoError)?;
577
578 local_file
579 .write_all(&contents)
580 .await
581 .map_err(crate::Error::IoError)?;
582 local_file.flush().await.map_err(crate::Error::IoError)?;
583
584 Ok(())
585 }
586
587 pub async fn execute(&self, command: &str) -> Result<CommandExecutedResult, crate::Error> {
600 let mut stdout_buffer = vec![];
601 let mut stderr_buffer = vec![];
602 let mut channel = self.connection_handle.channel_open_session().await?;
603 channel.exec(true, command).await?;
604
605 let mut result: Option<u32> = None;
606
607 while let Some(msg) = channel.wait().await {
609 match msg {
611 russh::ChannelMsg::Data { ref data } => {
613 stdout_buffer.write_all(data).await.unwrap()
614 }
615 russh::ChannelMsg::ExtendedData { ref data, ext } => {
616 if ext == 1 {
617 stderr_buffer.write_all(data).await.unwrap()
618 }
619 }
620
621 russh::ChannelMsg::ExitStatus { exit_status } => result = Some(exit_status),
625
626 _ => {}
631 }
632 }
633
634 if let Some(result) = result {
636 Ok(CommandExecutedResult {
637 stdout: String::from_utf8_lossy(&stdout_buffer).to_string(),
638 stderr: String::from_utf8_lossy(&stderr_buffer).to_string(),
639 exit_status: result,
640 })
641
642 } else {
644 Err(crate::Error::CommandDidntExit)
645 }
646 }
647
648 #[deprecated(
656 since = "0.11.0",
657 note = "Use execute_io with channels directly for more flexibility.\n\
658 This method will be removed or introduced breaking changes in future versions.\n\
659 At minimum, SteamingOutput will be renamed to StreamingOutput"
660 )]
661 pub async fn execute_streaming(
662 &self,
663 command: &str,
664 ch: tokio::sync::mpsc::Sender<SteamingOutput>,
665 ) -> Result<u32, crate::Error> {
666 let (stdout_tx, mut stdout_rx) = tokio::sync::mpsc::channel(1);
667 let (stderr_tx, mut stderr_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(1);
668
669 let exec_future = self.execute_io(command, stdout_tx, Some(stderr_tx), None, false, None);
670 tokio::pin!(exec_future);
671 let result = loop {
672 tokio::select! {
673 result = &mut exec_future => break result,
674 Some(stdout) = stdout_rx.recv() => {
675 ch.send(SteamingOutput::Stdout(stdout)).await.unwrap();
676 },
677 Some(stderr) = stderr_rx.recv() => {
678 ch.send(SteamingOutput::Stderr(stderr)).await.unwrap();
679 },
680 };
681 }?;
682 if let Some(stdout) = stdout_rx.recv().await {
684 ch.send(SteamingOutput::Stdout(stdout)).await.unwrap();
685 }
686 if let Some(stderr) = stderr_rx.recv().await {
687 ch.send(SteamingOutput::Stderr(stderr)).await.unwrap();
688 }
689 ch.send(SteamingOutput::ExitStatus(result)).await.unwrap();
690 Ok(result)
691 }
692
693 pub async fn execute_io(
756 &self,
757 command: &str,
758 stdout_channel: mpsc::Sender<Vec<u8>>,
759 stderr_channel: Option<mpsc::Sender<Vec<u8>>>,
760 mut stdin_channel: Option<mpsc::Receiver<Vec<u8>>>,
761 request_pty: bool,
762 default_exit_code: Option<u32>,
763 ) -> Result<u32, crate::Error> {
764 let mut channel = self.connection_handle.channel_open_session().await?;
765
766 let mut result: Option<u32> = None;
767 if request_pty {
768 channel
769 .request_pty(false, "xterm", 80_u32, 24_u32, 0, 0, &[])
770 .await?;
771 }
772
773 channel.exec(true, command).await?;
774
775 loop {
777 let recv_stdin = async {
778 if let Some(ch) = stdin_channel.as_mut() {
779 Some(ch.recv().await)
780 } else {
781 None
782 }
783 };
784 tokio::select! {
785 Some(input) = recv_stdin => {
786 if let Some(input) = input {
787 if input.is_empty() {
788 channel.eof().await? ;
789 } else {
790 channel.data(&input as &[u8]).await?;
791 }
792 }
793 },
794 msg = channel.wait() => {
795 match msg {
797 Some(russh::ChannelMsg::Data { ref data }) => {
799 stdout_channel
801 .send(data.to_vec())
802 .await
803 .map_err(crate::Error::ChannelSendError)?;
804 }
805 Some (russh::ChannelMsg::ExtendedData { ref data, ext }) => {
806 if ext == 1 {
807 if let Some(stderr_channel) = &stderr_channel {
808 stderr_channel
810 .send(data.to_vec())
811 .await
812 .map_err(crate::Error::ChannelSendError)?;
813 } else {
814 stdout_channel
816 .send(data.to_vec())
817 .await
818 .map_err(crate::Error::ChannelSendError)?;
819 }
820 }
821 }
822
823 Some (russh::ChannelMsg::ExitStatus { exit_status }) => result = Some(exit_status),
827
828 Some (_) => {},
833 None => break,
834 }
835 }
836 }
837 }
838
839 if let Some(result) = result {
841 Ok(result)
842 } else if let Some(default_exit_code) = default_exit_code {
844 Ok(default_exit_code)
845 } else {
847 Err(crate::Error::CommandDidntExit)
848 }
849 }
850
851 pub fn get_connection_username(&self) -> &String {
853 &self.username
854 }
855
856 pub fn get_connection_address(&self) -> &SocketAddr {
858 &self.address
859 }
860
861 pub async fn disconnect(&self) -> Result<(), crate::Error> {
862 self.connection_handle
863 .disconnect(russh::Disconnect::ByApplication, "", "")
864 .await
865 .map_err(crate::Error::SshError)
866 }
867
868 pub fn is_closed(&self) -> bool {
869 self.connection_handle.is_closed()
870 }
871}
872
873impl Debug for Client {
874 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
875 f.debug_struct("Client")
876 .field("username", &self.username)
877 .field("address", &self.address)
878 .field("connection_handle", &"Handle<ClientHandler>")
879 .finish()
880 }
881}
882
883#[derive(Debug, Clone, PartialEq, Eq, Hash)]
884pub struct CommandExecutedResult {
885 pub stdout: String,
887 pub stderr: String,
889 pub exit_status: u32,
891}
892
893#[derive(Debug, Clone)]
894struct ClientHandler {
895 hostname: String,
896 host: SocketAddr,
897 server_check: ServerCheckMethod,
898}
899
900impl Handler for ClientHandler {
901 type Error = crate::Error;
902
903 async fn check_server_key(
904 &mut self,
905 server_public_key: &russh::keys::PublicKey,
906 ) -> Result<bool, Self::Error> {
907 match &self.server_check {
908 ServerCheckMethod::NoCheck => Ok(true),
909 ServerCheckMethod::PublicKey(key) => {
910 let pk = russh::keys::parse_public_key_base64(key)
911 .map_err(|_| crate::Error::ServerCheckFailed)?;
912
913 Ok(pk == *server_public_key)
914 }
915 ServerCheckMethod::PublicKeyFile(key_file_name) => {
916 let pk = russh::keys::load_public_key(key_file_name)
917 .map_err(|_| crate::Error::ServerCheckFailed)?;
918
919 Ok(pk == *server_public_key)
920 }
921 ServerCheckMethod::KnownHostsFile(known_hosts_path) => {
922 let result = russh::keys::check_known_hosts_path(
923 &self.hostname,
924 self.host.port(),
925 server_public_key,
926 known_hosts_path,
927 )
928 .map_err(|_| crate::Error::ServerCheckFailed)?;
929
930 Ok(result)
931 }
932 ServerCheckMethod::DefaultKnownHostsFile => {
933 let result = russh::keys::check_known_hosts(
934 &self.hostname,
935 self.host.port(),
936 server_public_key,
937 )
938 .map_err(|_| crate::Error::ServerCheckFailed)?;
939
940 Ok(result)
941 }
942 }
943 }
944}
945
946#[cfg(test)]
947mod tests {
948 #![allow(deprecated, clippy::useless_vec)]
949
950 use crate::client::*;
951 use core::time;
952 use dotenv::dotenv;
953 use std::path::Path;
954 use std::sync::Once;
955 use tokio::io::AsyncReadExt;
956 static INIT: Once = Once::new();
957
958 fn initialize() {
959 println!("Running initialization code before tests...");
961 if is_running_in_docker() {
963 println!("Running inside Docker.");
964 } else {
965 println!("Not running inside Docker. Load env from file");
966 dotenv().ok();
967 }
968 }
969 fn is_running_in_docker() -> bool {
970 Path::new("/.dockerenv").exists() || check_cgroup()
971 }
972
973 fn check_cgroup() -> bool {
974 match std::fs::read_to_string("/proc/1/cgroup") {
975 Ok(contents) => contents.contains("docker"),
976 Err(_) => false,
977 }
978 }
979
980 fn env(name: &str) -> String {
981 INIT.call_once(|| {
982 initialize();
983 });
984 std::env::var(name).unwrap_or_else(|_| {
985 panic!(
986 "Failed to get env var needed for test, make sure to set the following env var: {name}",
987 )
988 })
989 }
990
991 fn test_address() -> SocketAddr {
992 format!(
993 "{}:{}",
994 env("ASYNC_SSH2_TEST_HOST_IP"),
995 env("ASYNC_SSH2_TEST_HOST_PORT")
996 )
997 .parse()
998 .unwrap()
999 }
1000
1001 fn test_hostname() -> impl ToSocketAddrsWithHostname {
1002 (
1003 env("ASYNC_SSH2_TEST_HOST_NAME"),
1004 env("ASYNC_SSH2_TEST_HOST_PORT").parse().unwrap(),
1005 )
1006 }
1007
1008 async fn establish_test_host_connection() -> Client {
1009 Client::connect(
1010 (
1011 env("ASYNC_SSH2_TEST_HOST_IP"),
1012 env("ASYNC_SSH2_TEST_HOST_PORT").parse().unwrap(),
1013 ),
1014 &env("ASYNC_SSH2_TEST_HOST_USER"),
1015 AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1016 ServerCheckMethod::NoCheck,
1017 )
1018 .await
1019 .expect("Connection/Authentification failed")
1020 }
1021
1022 #[tokio::test]
1023 async fn connect_with_password() {
1024 let client = establish_test_host_connection().await;
1025 assert_eq!(
1026 &env("ASYNC_SSH2_TEST_HOST_USER"),
1027 client.get_connection_username(),
1028 );
1029 assert_eq!(test_address(), *client.get_connection_address(),);
1030 }
1031
1032 #[tokio::test]
1033 async fn execute_command_result() {
1034 let client = establish_test_host_connection().await;
1035 let output = client.execute("echo test!!!").await.unwrap();
1036 assert_eq!("test!!!\n", output.stdout);
1037 assert_eq!("", output.stderr);
1038 assert_eq!(0, output.exit_status);
1039 }
1040
1041 #[tokio::test]
1042 async fn execute_streaming_command_result() {
1043 let (tx, mut rx) = tokio::sync::mpsc::channel(10);
1044 let client = establish_test_host_connection().await;
1045 let result = client.execute_streaming("echo test!!!", tx).await.unwrap();
1046 let mut output = Vec::new();
1047 while let Some(msg) = rx.recv().await {
1048 output.push(msg);
1049 }
1050 assert_eq!(0, result);
1051 assert_eq!(
1052 &[
1053 SteamingOutput::Stdout(b"test!!!\n".to_vec()),
1054 SteamingOutput::ExitStatus(0),
1055 ],
1056 output.as_slice(),
1057 );
1058 }
1059
1060 #[tokio::test]
1061 async fn execute_command_result_stderr() {
1062 let client = establish_test_host_connection().await;
1063 let output = client.execute("echo test!!! 1>&2").await.unwrap();
1064 assert_eq!("", output.stdout);
1065 assert_eq!("test!!!\n", output.stderr);
1066 assert_eq!(0, output.exit_status);
1067 }
1068
1069 #[tokio::test]
1070 async fn execute_streaming_command_result_stderr() {
1071 let client = establish_test_host_connection().await;
1072 let (tx, mut rx) = tokio::sync::mpsc::channel(10);
1073 let result = client
1074 .execute_streaming("echo test!!! 1>&2", tx)
1075 .await
1076 .unwrap();
1077 let mut output = Vec::new();
1078 while let Some(msg) = rx.recv().await {
1079 output.push(msg);
1080 }
1081 assert_eq!(0, result);
1082 assert_eq!(
1083 &[
1084 SteamingOutput::Stderr(b"test!!!\n".to_vec()),
1085 SteamingOutput::ExitStatus(0),
1086 ],
1087 output.as_slice()
1088 );
1089 }
1090
1091 #[tokio::test]
1092 async fn unicode_output() {
1093 let client = establish_test_host_connection().await;
1094 let output = client.execute("echo To thḙ moon! 🚀").await.unwrap();
1095 assert_eq!("To thḙ moon! 🚀\n", output.stdout);
1096 assert_eq!(0, output.exit_status);
1097 }
1098
1099 #[tokio::test]
1100 async fn execute_command_status() {
1101 let client = establish_test_host_connection().await;
1102 let output = client.execute("exit 42").await.unwrap();
1103 assert_eq!(42, output.exit_status);
1104 }
1105
1106 #[tokio::test]
1107 async fn execute_streaming_command_status() {
1108 let client = establish_test_host_connection().await;
1109 let (tx, mut rx) = tokio::sync::mpsc::channel(10);
1110 let result = client.execute_streaming("exit 42", tx).await.unwrap();
1111 let mut output = Vec::new();
1112 while let Some(msg) = rx.recv().await {
1113 output.push(msg);
1114 }
1115 assert_eq!(42, result);
1116 assert_eq!(&[SteamingOutput::ExitStatus(42),], output.as_slice());
1117 }
1118
1119 #[tokio::test]
1120 async fn execute_io_command() {
1121 let client = establish_test_host_connection().await;
1122 let (stdout_tx, mut stdout_rx) = tokio::sync::mpsc::channel(10);
1123 let (stderr_tx, mut stderr_rx) = tokio::sync::mpsc::channel(10);
1124 let cmd = "echo out1; echo err1 1>&2; echo out2; echo err2 1>&2; exit 7";
1125 let exec_future = client.execute_io(cmd, stdout_tx, Some(stderr_tx), None, false, None);
1126 tokio::pin!(exec_future);
1127 let mut result: Option<u32> = None;
1128 let mut stdout_output = vec![];
1129 let mut stderr_output = vec![];
1130 loop {
1131 tokio::select! {
1132 result_inner = &mut exec_future => {
1133 result = Some(result_inner.unwrap());
1134 },
1135 Some(stdout) = stdout_rx.recv() => {
1136 stdout_output.push(stdout);
1137 },
1138 Some(stderr) = stderr_rx.recv() => {
1139 stderr_output.push(stderr);
1140 },
1141 };
1142 if result.is_some() {
1143 break;
1144 }
1145 }
1146 assert_eq!(Some(7), result);
1147 assert_eq!(
1148 vec![b"out1\n".to_vec(), b"out2\n".to_vec()].concat(),
1149 stdout_output.concat()
1150 );
1151 assert_eq!(
1152 vec![b"err1\n".to_vec(), b"err2\n".to_vec()].concat(),
1153 stderr_output.concat()
1154 );
1155 }
1156
1157 #[tokio::test]
1158 async fn execute_multiple_commands() {
1159 let client = establish_test_host_connection().await;
1160 let output = client.execute("echo test!!!").await.unwrap().stdout;
1161 assert_eq!("test!!!\n", output);
1162
1163 let output = client.execute("echo Hello World").await.unwrap().stdout;
1164 assert_eq!("Hello World\n", output);
1165 }
1166
1167 #[tokio::test]
1168 async fn direct_tcpip_channel() {
1169 let client = establish_test_host_connection().await;
1170 let channel = client
1171 .open_direct_tcpip_channel(
1172 format!(
1173 "{}:{}",
1174 env("ASYNC_SSH2_TEST_HTTP_SERVER_IP"),
1175 env("ASYNC_SSH2_TEST_HTTP_SERVER_PORT"),
1176 ),
1177 None,
1178 )
1179 .await
1180 .unwrap();
1181
1182 let mut stream = channel.into_stream();
1183 stream.write_all(b"GET / HTTP/1.0\r\n\r\n").await.unwrap();
1184
1185 let mut response = String::new();
1186 stream.read_to_string(&mut response).await.unwrap();
1187
1188 let body = response.split_once("\r\n\r\n").unwrap().1;
1189 assert_eq!("Hello", body);
1190 }
1191
1192 #[tokio::test]
1193 async fn stderr_redirection() {
1194 let client = establish_test_host_connection().await;
1195
1196 let output = client.execute("echo foo >/dev/null").await.unwrap();
1197 assert_eq!("", output.stdout);
1198
1199 let output = client.execute("echo foo >>/dev/stderr").await.unwrap();
1200 assert_eq!("", output.stdout);
1201
1202 let output = client.execute("2>&1 echo foo >>/dev/stderr").await.unwrap();
1203 assert_eq!("foo\n", output.stdout);
1204 }
1205
1206 #[tokio::test]
1207 async fn sequential_commands() {
1208 let client = establish_test_host_connection().await;
1209
1210 for i in 0..100 {
1211 std::thread::sleep(time::Duration::from_millis(100));
1212 let res = client
1213 .execute(&format!("echo {i}"))
1214 .await
1215 .unwrap_or_else(|_| panic!("Execution failed in iteration {i}"));
1216 assert_eq!(format!("{i}\n"), res.stdout);
1217 }
1218 }
1219
1220 #[tokio::test]
1221 async fn execute_multiple_context() {
1222 let client = establish_test_host_connection().await;
1224 let output = client
1225 .execute("export VARIABLE=42; echo $VARIABLE")
1226 .await
1227 .unwrap()
1228 .stdout;
1229 assert_eq!("42\n", output);
1230
1231 let output = client.execute("echo $VARIABLE").await.unwrap().stdout;
1232 assert_eq!("\n", output);
1233 }
1234
1235 #[tokio::test]
1236 async fn connect_second_address() {
1237 let client = Client::connect(
1238 &[SocketAddr::from(([127, 0, 0, 1], 23)), test_address()][..],
1239 &env("ASYNC_SSH2_TEST_HOST_USER"),
1240 AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1241 ServerCheckMethod::NoCheck,
1242 )
1243 .await
1244 .expect("Resolution to second address failed");
1245
1246 assert_eq!(test_address(), *client.get_connection_address(),);
1247 }
1248
1249 #[tokio::test]
1250 async fn connect_with_wrong_password() {
1251 let error = Client::connect(
1252 test_address(),
1253 &env("ASYNC_SSH2_TEST_HOST_USER"),
1254 AuthMethod::with_password("hopefully the wrong password"),
1255 ServerCheckMethod::NoCheck,
1256 )
1257 .await
1258 .expect_err("Client connected with wrong password");
1259
1260 match error {
1261 crate::Error::PasswordWrong => {}
1262 _ => panic!("Wrong error type"),
1263 }
1264 }
1265
1266 #[tokio::test]
1267 async fn invalid_address() {
1268 let no_client = Client::connect(
1269 "this is definitely not an address",
1270 &env("ASYNC_SSH2_TEST_HOST_USER"),
1271 AuthMethod::with_password("hopefully the wrong password"),
1272 ServerCheckMethod::NoCheck,
1273 )
1274 .await;
1275 assert!(no_client.is_err());
1276 }
1277
1278 #[tokio::test]
1279 async fn connect_to_wrong_port() {
1280 let no_client = Client::connect(
1281 (env("ASYNC_SSH2_TEST_HOST_IP"), 23),
1282 &env("ASYNC_SSH2_TEST_HOST_USER"),
1283 AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1284 ServerCheckMethod::NoCheck,
1285 )
1286 .await;
1287 assert!(no_client.is_err());
1288 }
1289
1290 #[tokio::test]
1291 #[ignore = "This times out only after 20 seconds"]
1292 async fn connect_to_wrong_host() {
1293 let no_client = Client::connect(
1294 "172.16.0.6:22",
1295 "xxx",
1296 AuthMethod::with_password("xxx"),
1297 ServerCheckMethod::NoCheck,
1298 )
1299 .await;
1300 assert!(no_client.is_err());
1301 }
1302
1303 #[tokio::test]
1304 async fn auth_key_file() {
1305 let client = Client::connect(
1306 test_address(),
1307 &env("ASYNC_SSH2_TEST_HOST_USER"),
1308 AuthMethod::with_key_file(env("ASYNC_SSH2_TEST_CLIENT_PRIV"), None),
1309 ServerCheckMethod::NoCheck,
1310 )
1311 .await;
1312 assert!(client.is_ok());
1313 }
1314
1315 #[tokio::test]
1316 #[cfg(not(target_os = "windows"))]
1317 async fn auth_with_agent() {
1318 let client = Client::connect(
1321 test_address(),
1322 &env("ASYNC_SSH2_TEST_HOST_USER"),
1323 AuthMethod::with_agent(),
1324 ServerCheckMethod::NoCheck,
1325 )
1326 .await
1327 .expect("Agent authentication should succeed with correct key loaded");
1328
1329 let output = client.execute("echo test").await.unwrap();
1331 assert_eq!("test\n", output.stdout);
1332 }
1333
1334 #[tokio::test]
1335 #[cfg(not(target_os = "windows"))]
1336 async fn auth_with_agent_wrong_user() {
1337 let result = Client::connect(
1339 test_address(),
1340 "wrong_user_that_does_not_exist",
1341 AuthMethod::with_agent(),
1342 ServerCheckMethod::NoCheck,
1343 )
1344 .await;
1345
1346 assert!(matches!(
1348 result,
1349 Err(crate::Error::AgentAuthenticationFailed)
1350 ));
1351 }
1352
1353 #[tokio::test]
1354 #[cfg(not(target_os = "windows"))]
1355 async fn auth_with_agent_no_sock() {
1356 let original_sock = std::env::var("SSH_AUTH_SOCK").ok();
1359 unsafe {
1360 std::env::remove_var("SSH_AUTH_SOCK");
1361 }
1362
1363 let result = Client::connect(
1364 test_address(),
1365 &env("ASYNC_SSH2_TEST_HOST_USER"),
1366 AuthMethod::with_agent(),
1367 ServerCheckMethod::NoCheck,
1368 )
1369 .await;
1370
1371 if let Some(sock) = original_sock {
1373 unsafe {
1374 std::env::set_var("SSH_AUTH_SOCK", sock);
1375 }
1376 }
1377
1378 assert!(matches!(result, Err(crate::Error::AgentConnectionFailed)));
1380 }
1381
1382 #[tokio::test]
1383 async fn auth_key_file_with_passphrase() {
1384 let client = Client::connect(
1385 test_address(),
1386 &env("ASYNC_SSH2_TEST_HOST_USER"),
1387 AuthMethod::with_key_file(
1388 env("ASYNC_SSH2_TEST_CLIENT_PROT_PRIV"),
1389 Some(&env("ASYNC_SSH2_TEST_CLIENT_PROT_PASS")),
1390 ),
1391 ServerCheckMethod::NoCheck,
1392 )
1393 .await;
1394 if client.is_err() {
1395 println!("{:?}", client.err());
1396 panic!();
1397 }
1398 assert!(client.is_ok());
1399 }
1400
1401 #[tokio::test]
1402 async fn auth_key_str() {
1403 let key = std::fs::read_to_string(env("ASYNC_SSH2_TEST_CLIENT_PRIV")).unwrap();
1404
1405 let client = Client::connect(
1406 test_address(),
1407 &env("ASYNC_SSH2_TEST_HOST_USER"),
1408 AuthMethod::with_key(key.as_str(), None),
1409 ServerCheckMethod::NoCheck,
1410 )
1411 .await;
1412 assert!(client.is_ok());
1413 }
1414
1415 #[tokio::test]
1416 async fn auth_key_str_with_passphrase() {
1417 let key = std::fs::read_to_string(env("ASYNC_SSH2_TEST_CLIENT_PROT_PRIV")).unwrap();
1418
1419 let client = Client::connect(
1420 test_address(),
1421 &env("ASYNC_SSH2_TEST_HOST_USER"),
1422 AuthMethod::with_key(key.as_str(), Some(&env("ASYNC_SSH2_TEST_CLIENT_PROT_PASS"))),
1423 ServerCheckMethod::NoCheck,
1424 )
1425 .await;
1426 assert!(client.is_ok());
1427 }
1428
1429 #[tokio::test]
1430 async fn auth_keyboard_interactive() {
1431 let client = Client::connect(
1432 test_address(),
1433 &env("ASYNC_SSH2_TEST_HOST_USER"),
1434 AuthKeyboardInteractive::new()
1435 .with_response("Password", env("ASYNC_SSH2_TEST_HOST_PW"))
1436 .into(),
1437 ServerCheckMethod::NoCheck,
1438 )
1439 .await;
1440 assert!(client.is_ok());
1441 }
1442
1443 #[tokio::test]
1444 async fn auth_keyboard_interactive_exact() {
1445 let client = Client::connect(
1446 test_address(),
1447 &env("ASYNC_SSH2_TEST_HOST_USER"),
1448 AuthKeyboardInteractive::new()
1449 .with_response_exact("Password: ", env("ASYNC_SSH2_TEST_HOST_PW"))
1450 .into(),
1451 ServerCheckMethod::NoCheck,
1452 )
1453 .await;
1454 assert!(client.is_ok());
1455 }
1456
1457 #[tokio::test]
1458 async fn auth_keyboard_interactive_wrong_response() {
1459 let client = Client::connect(
1460 test_address(),
1461 &env("ASYNC_SSH2_TEST_HOST_USER"),
1462 AuthKeyboardInteractive::new()
1463 .with_response_exact("Password: ", "wrong password")
1464 .into(),
1465 ServerCheckMethod::NoCheck,
1466 )
1467 .await;
1468 match client {
1469 Err(crate::error::Error::KeyboardInteractiveAuthFailed) => {}
1470 Err(e) => {
1471 panic!("Expected KeyboardInteractiveAuthFailed error. Got error: {e:?}")
1472 }
1473 Ok(_) => panic!("Expected KeyboardInteractiveAuthFailed error."),
1474 }
1475 }
1476
1477 #[tokio::test]
1478 async fn auth_keyboard_interactive_no_response() {
1479 let client = Client::connect(
1480 test_address(),
1481 &env("ASYNC_SSH2_TEST_HOST_USER"),
1482 AuthKeyboardInteractive::new()
1483 .with_response_exact("Password:", "123")
1484 .into(),
1485 ServerCheckMethod::NoCheck,
1486 )
1487 .await;
1488 match client {
1489 Err(crate::error::Error::KeyboardInteractiveNoResponseForPrompt(prompt)) => {
1490 assert_eq!(prompt, "Password: ");
1491 }
1492 Err(e) => {
1493 panic!("Expected KeyboardInteractiveNoResponseForPrompt error. Got error: {e:?}")
1494 }
1495 Ok(_) => panic!("Expected KeyboardInteractiveNoResponseForPrompt error."),
1496 }
1497 }
1498
1499 #[tokio::test]
1500 async fn server_check_file() {
1501 let client = Client::connect(
1502 test_address(),
1503 &env("ASYNC_SSH2_TEST_HOST_USER"),
1504 AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1505 ServerCheckMethod::with_public_key_file(&env("ASYNC_SSH2_TEST_SERVER_PUB")),
1506 )
1507 .await;
1508 assert!(client.is_ok());
1509 }
1510
1511 #[tokio::test]
1512 async fn server_check_str() {
1513 let line = std::fs::read_to_string(env("ASYNC_SSH2_TEST_SERVER_PUB")).unwrap();
1514 let mut split = line.split_whitespace();
1515 let key = match (split.next(), split.next()) {
1516 (Some(_), Some(k)) => k,
1517 (Some(k), None) => k,
1518 _ => panic!("Failed to parse pub key file"),
1519 };
1520
1521 let client = Client::connect(
1522 test_address(),
1523 &env("ASYNC_SSH2_TEST_HOST_USER"),
1524 AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1525 ServerCheckMethod::with_public_key(key),
1526 )
1527 .await;
1528 assert!(client.is_ok());
1529 }
1530
1531 #[tokio::test]
1532 async fn server_check_by_known_hosts_for_ip() {
1533 let client = Client::connect(
1534 test_address(),
1535 &env("ASYNC_SSH2_TEST_HOST_USER"),
1536 AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1537 ServerCheckMethod::with_known_hosts_file(&env("ASYNC_SSH2_TEST_KNOWN_HOSTS")),
1538 )
1539 .await;
1540 assert!(client.is_ok());
1541 }
1542
1543 #[tokio::test]
1544 async fn server_check_by_known_hosts_for_hostname() {
1545 let client = Client::connect(
1546 test_hostname(),
1547 &env("ASYNC_SSH2_TEST_HOST_USER"),
1548 AuthMethod::with_password(&env("ASYNC_SSH2_TEST_HOST_PW")),
1549 ServerCheckMethod::with_known_hosts_file(&env("ASYNC_SSH2_TEST_KNOWN_HOSTS")),
1550 )
1551 .await;
1552 if is_running_in_docker() {
1553 assert!(client.is_ok());
1554 } else {
1555 assert!(client.is_err()); }
1557 }
1558
1559 #[tokio::test]
1560 async fn client_can_be_cloned() {
1561 let client = establish_test_host_connection().await;
1562 let client2 = client.clone();
1563
1564 let result1 = client.execute("echo test clone").await.unwrap();
1565 let result2 = client2.execute("echo test clone2").await.unwrap();
1566
1567 assert_eq!(result1.stdout, "test clone\n");
1568 assert_eq!(result2.stdout, "test clone2\n");
1569 }
1570
1571 #[tokio::test]
1572 async fn client_can_upload_file() {
1573 let client = establish_test_host_connection().await;
1574 client
1575 .upload_file(&env("ASYNC_SSH2_TEST_UPLOAD_FILE"), "/tmp/uploaded")
1576 .await
1577 .unwrap();
1578 let result = client.execute("cat /tmp/uploaded").await.unwrap();
1579 assert_eq!(result.stdout, "this is a test file\n");
1580 }
1581
1582 #[tokio::test]
1583 async fn client_can_download_file() {
1584 let client = establish_test_host_connection().await;
1585
1586 client
1587 .execute("echo 'this is a downloaded test file' > /tmp/test_download")
1588 .await
1589 .unwrap();
1590
1591 let local_path = std::env::temp_dir().join("downloaded_test_file");
1592 client
1593 .download_file("/tmp/test_download", &local_path)
1594 .await
1595 .unwrap();
1596
1597 let contents = tokio::fs::read_to_string(&local_path).await.unwrap();
1598 assert_eq!(contents, "this is a downloaded test file\n");
1599 }
1600}