@@ -22,7 +22,7 @@ use rustyline::config::Configurer;
2222use rustyline:: error:: ReadlineError ;
2323use rustyline:: DefaultEditor ;
2424use std:: io:: { self , Write } ;
25- use std:: path:: PathBuf ;
25+ use std:: path:: { Path , PathBuf } ;
2626use std:: sync:: atomic:: { AtomicBool , Ordering } ;
2727use std:: sync:: Arc ;
2828use tokio:: sync:: mpsc;
@@ -52,6 +52,11 @@ pub struct InteractiveCommand {
5252 pub config : Config ,
5353 pub interactive_config : InteractiveConfig ,
5454 pub cluster_name : Option < String > ,
55+ // Authentication parameters (consistent with exec mode)
56+ pub key_path : Option < PathBuf > ,
57+ pub use_agent : bool ,
58+ pub use_password : bool ,
59+ pub strict_mode : StrictHostKeyChecking ,
5560}
5661
5762/// Result of an interactive session
@@ -194,11 +199,11 @@ impl InteractiveCommand {
194199
195200 /// Connect to a single node and establish an interactive shell
196201 async fn connect_to_node ( & self , node : Node ) -> Result < NodeSession > {
197- // Determine authentication method
202+ // Determine authentication method using the same logic as exec mode
198203 let auth_method = self . determine_auth_method ( & node) ?;
199204
200- // Set up host key checking
201- let check_method = get_check_method ( StrictHostKeyChecking :: AcceptNew ) ;
205+ // Set up host key checking using the configured strict mode
206+ let check_method = get_check_method ( self . strict_mode ) ;
202207
203208 // Connect with timeout
204209 let addr = ( node. host . as_str ( ) , node. port ) ;
@@ -252,29 +257,122 @@ impl InteractiveCommand {
252257 } )
253258 }
254259
255- /// Determine authentication method based on node and config
260+ /// Determine authentication method based on node and config (same logic as exec mode)
256261 fn determine_auth_method ( & self , node : & Node ) -> Result < AuthMethod > {
257- // Check if SSH agent is available
258- if std:: env:: var ( "SSH_AUTH_SOCK" ) . is_ok ( ) {
262+ // If password authentication is explicitly requested
263+ if self . use_password {
264+ tracing:: debug!( "Using password authentication" ) ;
265+ let password = rpassword:: prompt_password ( format ! (
266+ "Enter password for {}@{}: " ,
267+ node. username, node. host
268+ ) )
269+ . with_context ( || "Failed to read password" ) ?;
270+ return Ok ( AuthMethod :: with_password ( & password) ) ;
271+ }
272+
273+ // If SSH agent is explicitly requested, try that first
274+ if self . use_agent {
275+ #[ cfg( not( target_os = "windows" ) ) ]
276+ {
277+ // Check if SSH_AUTH_SOCK is available
278+ if std:: env:: var ( "SSH_AUTH_SOCK" ) . is_ok ( ) {
279+ tracing:: debug!( "Using SSH agent for authentication" ) ;
280+ return Ok ( AuthMethod :: Agent ) ;
281+ }
282+ tracing:: warn!(
283+ "SSH agent requested but SSH_AUTH_SOCK environment variable not set"
284+ ) ;
285+ // Fall through to key file authentication
286+ }
287+ #[ cfg( target_os = "windows" ) ]
288+ {
289+ anyhow:: bail!( "SSH agent authentication is not supported on Windows" ) ;
290+ }
291+ }
292+
293+ // Try key file authentication
294+ if let Some ( ref key_path) = self . key_path {
295+ tracing:: debug!( "Authenticating with key: {:?}" , key_path) ;
296+
297+ // Check if the key is encrypted by attempting to read it
298+ let key_contents = std:: fs:: read_to_string ( key_path)
299+ . with_context ( || format ! ( "Failed to read SSH key file: {key_path:?}" ) ) ?;
300+
301+ let passphrase = if key_contents. contains ( "ENCRYPTED" )
302+ || key_contents. contains ( "Proc-Type: 4,ENCRYPTED" )
303+ {
304+ tracing:: debug!( "Detected encrypted SSH key, prompting for passphrase" ) ;
305+ let pass =
306+ rpassword:: prompt_password ( format ! ( "Enter passphrase for key {key_path:?}: " ) )
307+ . with_context ( || "Failed to read passphrase" ) ?;
308+ Some ( pass)
309+ } else {
310+ None
311+ } ;
312+
313+ return Ok ( AuthMethod :: with_key_file ( key_path, passphrase. as_deref ( ) ) ) ;
314+ }
315+
316+ // If no explicit key path, try SSH agent if available (auto-detect)
317+ #[ cfg( not( target_os = "windows" ) ) ]
318+ if !self . use_agent && std:: env:: var ( "SSH_AUTH_SOCK" ) . is_ok ( ) {
319+ tracing:: debug!( "SSH agent detected, attempting agent authentication" ) ;
259320 return Ok ( AuthMethod :: Agent ) ;
260321 }
261322
262- // Try to find SSH key
263- let ssh_key_paths = vec ! [
264- dirs:: home_dir( ) . map( |h| h. join( ".ssh/id_rsa" ) ) ,
265- dirs:: home_dir( ) . map( |h| h. join( ".ssh/id_ed25519" ) ) ,
323+ // Fallback to default key locations
324+ let home = std:: env:: var ( "HOME" ) . unwrap_or_else ( |_| "." . to_string ( ) ) ;
325+ let home_path = Path :: new ( & home) . join ( ".ssh" ) ;
326+
327+ // Try common key files in order of preference
328+ let default_keys = [
329+ home_path. join ( "id_ed25519" ) ,
330+ home_path. join ( "id_rsa" ) ,
331+ home_path. join ( "id_ecdsa" ) ,
332+ home_path. join ( "id_dsa" ) ,
266333 ] ;
267334
268- for key_path in ssh_key_paths. into_iter ( ) . flatten ( ) {
269- if key_path. exists ( ) {
270- return Ok ( AuthMethod :: with_key_file ( key_path, None ) ) ;
335+ for default_key in & default_keys {
336+ if default_key. exists ( ) {
337+ tracing:: debug!( "Using default key: {:?}" , default_key) ;
338+
339+ // Check if the key is encrypted
340+ let key_contents = std:: fs:: read_to_string ( default_key)
341+ . with_context ( || format ! ( "Failed to read SSH key file: {default_key:?}" ) ) ?;
342+
343+ let passphrase = if key_contents. contains ( "ENCRYPTED" )
344+ || key_contents. contains ( "Proc-Type: 4,ENCRYPTED" )
345+ {
346+ tracing:: debug!( "Detected encrypted SSH key, prompting for passphrase" ) ;
347+ let pass = rpassword:: prompt_password ( format ! (
348+ "Enter passphrase for key {default_key:?}: "
349+ ) )
350+ . with_context ( || "Failed to read passphrase" ) ?;
351+ Some ( pass)
352+ } else {
353+ None
354+ } ;
355+
356+ return Ok ( AuthMethod :: with_key_file (
357+ default_key,
358+ passphrase. as_deref ( ) ,
359+ ) ) ;
271360 }
272361 }
273362
274- // If no key found, prompt for password
275- let password =
276- rpassword:: prompt_password ( format ! ( "Password for {}@{}: " , node. username, node. host) ) ?;
277- Ok ( AuthMethod :: with_password ( & password) )
363+ anyhow:: bail!(
364+ "SSH authentication failed: No authentication method available.\n \
365+ Tried:\n \
366+ - SSH agent: {}\n \
367+ - SSH keys: {:?}\n \
368+ Please ensure you have a valid SSH key or SSH agent running.",
369+ if std:: env:: var( "SSH_AUTH_SOCK" ) . is_ok( ) {
370+ "Available but no identities"
371+ } else {
372+ "Not available (SSH_AUTH_SOCK not set)"
373+ } ,
374+ default_keys
375+ )
278376 }
279377
280378 /// Run interactive mode with a single node
@@ -832,6 +930,10 @@ mod tests {
832930 config : Config :: default ( ) ,
833931 interactive_config : InteractiveConfig :: default ( ) ,
834932 cluster_name : None ,
933+ key_path : None ,
934+ use_agent : false ,
935+ use_password : false ,
936+ strict_mode : StrictHostKeyChecking :: AcceptNew ,
835937 } ;
836938
837939 let path = PathBuf :: from ( "~/test/file.txt" ) ;
@@ -856,6 +958,10 @@ mod tests {
856958 config : Config :: default ( ) ,
857959 interactive_config : InteractiveConfig :: default ( ) ,
858960 cluster_name : None ,
961+ key_path : None ,
962+ use_agent : false ,
963+ use_password : false ,
964+ strict_mode : StrictHostKeyChecking :: AcceptNew ,
859965 } ;
860966
861967 let node = Node :: new ( String :: from ( "example.com" ) , 22 , String :: from ( "alice" ) ) ;
0 commit comments