Skip to content

Commit b289462

Browse files
authored
fix: align interactive mode authentication with exec mode (#17)
- Add authentication parameters (key_path, use_agent, use_password, strict_mode) to InteractiveCommand struct - Reuse the same authentication logic from exec mode's determine_auth_method - Pass CLI auth parameters from main.rs to interactive command - Update tests and examples to include new authentication fields This ensures consistent authentication behavior between interactive and exec modes, fixing connection failures when using cluster configurations with SSH keys.
1 parent 2707fd8 commit b289462

5 files changed

Lines changed: 188 additions & 18 deletions

File tree

examples/interactive_demo.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use bssh::commands::interactive::InteractiveCommand;
1818
use bssh::config::{Config, InteractiveConfig};
1919
use bssh::node::Node;
20+
use bssh::ssh::known_hosts::StrictHostKeyChecking;
2021
use std::path::PathBuf;
2122

2223
#[tokio::main]
@@ -48,6 +49,10 @@ async fn main() -> anyhow::Result<()> {
4849
config: Config::default(),
4950
interactive_config: InteractiveConfig::default(),
5051
cluster_name: None,
52+
key_path: None,
53+
use_agent: false,
54+
use_password: false,
55+
strict_mode: StrictHostKeyChecking::AcceptNew,
5156
};
5257

5358
println!("Starting interactive session...");

src/commands/interactive.rs

Lines changed: 124 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use rustyline::config::Configurer;
2222
use rustyline::error::ReadlineError;
2323
use rustyline::DefaultEditor;
2424
use std::io::{self, Write};
25-
use std::path::PathBuf;
25+
use std::path::{Path, PathBuf};
2626
use std::sync::atomic::{AtomicBool, Ordering};
2727
use std::sync::Arc;
2828
use 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"));

src/main.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,15 @@ async fn main() -> Result<()> {
241241

242242
let merged_work_dir = work_dir.or(interactive_config.work_dir.clone());
243243

244+
// Determine SSH key path: CLI argument takes precedence over config
245+
let key_path = if let Some(identity) = &cli.identity {
246+
Some(identity.clone())
247+
} else {
248+
config
249+
.get_ssh_key(actual_cluster_name.as_deref().or(cli.cluster.as_deref()))
250+
.map(|ssh_key| bssh::config::expand_tilde(Path::new(&ssh_key)))
251+
};
252+
244253
let interactive_cmd = InteractiveCommand {
245254
single_node: merged_mode.0,
246255
multiplex: merged_mode.1,
@@ -251,6 +260,10 @@ async fn main() -> Result<()> {
251260
config: config.clone(),
252261
interactive_config,
253262
cluster_name: cluster_name.map(String::from),
263+
key_path,
264+
use_agent: cli.use_agent,
265+
use_password: cli.password,
266+
strict_mode,
254267
};
255268
let result = interactive_cmd.execute().await?;
256269
println!("\nInteractive session ended.");

tests/interactive_integration_test.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use bssh::commands::interactive::InteractiveCommand;
1818
use bssh::config::{Config, InteractiveConfig};
1919
use bssh::node::Node;
20+
use bssh::ssh::known_hosts::StrictHostKeyChecking;
2021
use std::path::PathBuf;
2122
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
2223
use std::sync::Arc;
@@ -41,6 +42,10 @@ fn test_interactive_command_builder() {
4142
config: Config::default(),
4243
interactive_config: InteractiveConfig::default(),
4344
cluster_name: None,
45+
key_path: None,
46+
use_agent: false,
47+
use_password: false,
48+
strict_mode: StrictHostKeyChecking::AcceptNew,
4449
};
4550

4651
assert!(!cmd.single_node);
@@ -66,6 +71,10 @@ fn test_history_file_handling() {
6671
config: Config::default(),
6772
interactive_config: InteractiveConfig::default(),
6873
cluster_name: None,
74+
key_path: None,
75+
use_agent: false,
76+
use_password: false,
77+
strict_mode: StrictHostKeyChecking::AcceptNew,
6978
};
7079

7180
assert_eq!(cmd.history_file, history_path);
@@ -154,6 +163,10 @@ async fn test_interactive_with_unreachable_nodes() {
154163
config: Config::default(),
155164
interactive_config: InteractiveConfig::default(),
156165
cluster_name: None,
166+
key_path: None,
167+
use_agent: false,
168+
use_password: false,
169+
strict_mode: StrictHostKeyChecking::AcceptNew,
157170
};
158171

159172
// This should fail to connect
@@ -179,6 +192,10 @@ async fn test_interactive_with_no_nodes() {
179192
config: Config::default(),
180193
interactive_config: InteractiveConfig::default(),
181194
cluster_name: None,
195+
key_path: None,
196+
use_agent: false,
197+
use_password: false,
198+
strict_mode: StrictHostKeyChecking::AcceptNew,
182199
};
183200

184201
let result = cmd.execute().await;
@@ -214,6 +231,10 @@ fn test_mode_configuration() {
214231
config: Config::default(),
215232
interactive_config: InteractiveConfig::default(),
216233
cluster_name: None,
234+
key_path: None,
235+
use_agent: false,
236+
use_password: false,
237+
strict_mode: StrictHostKeyChecking::AcceptNew,
217238
};
218239

219240
assert!(single_cmd.single_node);
@@ -230,6 +251,10 @@ fn test_mode_configuration() {
230251
config: Config::default(),
231252
interactive_config: InteractiveConfig::default(),
232253
cluster_name: None,
254+
key_path: None,
255+
use_agent: false,
256+
use_password: false,
257+
strict_mode: StrictHostKeyChecking::AcceptNew,
233258
};
234259

235260
assert!(!multi_cmd.single_node);
@@ -249,6 +274,10 @@ fn test_working_directory_config() {
249274
config: Config::default(),
250275
interactive_config: InteractiveConfig::default(),
251276
cluster_name: None,
277+
key_path: None,
278+
use_agent: false,
279+
use_password: false,
280+
strict_mode: StrictHostKeyChecking::AcceptNew,
252281
};
253282

254283
assert_eq!(cmd_with_dir.work_dir, Some("/var/www".to_string()));
@@ -263,6 +292,10 @@ fn test_working_directory_config() {
263292
config: Config::default(),
264293
interactive_config: InteractiveConfig::default(),
265294
cluster_name: None,
295+
key_path: None,
296+
use_agent: false,
297+
use_password: false,
298+
strict_mode: StrictHostKeyChecking::AcceptNew,
266299
};
267300

268301
assert_eq!(cmd_without_dir.work_dir, None);
@@ -289,6 +322,10 @@ fn test_prompt_format() {
289322
config: Config::default(),
290323
interactive_config: InteractiveConfig::default(),
291324
cluster_name: None,
325+
key_path: None,
326+
use_agent: false,
327+
use_password: false,
328+
strict_mode: StrictHostKeyChecking::AcceptNew,
292329
};
293330

294331
assert_eq!(cmd.prompt_format, format);

tests/interactive_test.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use bssh::commands::interactive::InteractiveCommand;
1616
use bssh::config::{Config, InteractiveConfig};
1717
use bssh::node::Node;
18+
use bssh::ssh::known_hosts::StrictHostKeyChecking;
1819
use std::path::PathBuf;
1920

2021
#[tokio::test]
@@ -29,6 +30,10 @@ async fn test_interactive_command_creation() {
2930
config: Config::default(),
3031
interactive_config: InteractiveConfig::default(),
3132
cluster_name: None,
33+
key_path: None,
34+
use_agent: false,
35+
use_password: false,
36+
strict_mode: StrictHostKeyChecking::AcceptNew,
3237
};
3338

3439
assert!(!cmd.single_node);
@@ -48,6 +53,10 @@ async fn test_interactive_with_no_nodes() {
4853
config: Config::default(),
4954
interactive_config: InteractiveConfig::default(),
5055
cluster_name: None,
56+
key_path: None,
57+
use_agent: false,
58+
use_password: false,
59+
strict_mode: StrictHostKeyChecking::AcceptNew,
5160
};
5261

5362
let result = cmd.execute().await;

0 commit comments

Comments
 (0)