Skip to content

Commit 0a56484

Browse files
committed
feat: add password authentication and SSH key passphrase support
- Add --password flag for password-based authentication - Automatically detect and prompt for passphrases on encrypted SSH keys - Support passphrase entry for both explicit and default key paths - Check multiple default key types (ed25519, rsa, ecdsa, dsa) - Update all commands (exec, upload, download, ping) to support new auth methods - Add secure password/passphrase prompting using rpassword crate
1 parent d2c8341 commit 0a56484

5 files changed

Lines changed: 178 additions & 33 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ A high-performance parallel SSH command execution tool for cluster management, b
77
- **Parallel Execution**: Execute commands across multiple nodes simultaneously
88
- **Cluster Management**: Define and manage node clusters via configuration files
99
- **Progress Tracking**: Real-time progress indicators for each node
10-
- **Flexible Authentication**: Support for SSH keys and SSH agent
10+
- **Flexible Authentication**: Support for SSH keys, SSH agent, password authentication, and encrypted key passphrases
1111
- **Host Key Verification**: Secure host key checking with known_hosts support
1212
- **Cross-Platform**: Works on Linux and macOS
1313
- **Output Management**: Save command outputs to files per node with detailed logging
@@ -35,6 +35,12 @@ bssh -c staging -i ~/.ssh/custom_key "systemctl status nginx"
3535
# Use SSH agent for authentication
3636
bssh --use-agent -c production "systemctl status nginx"
3737

38+
# Use password authentication (will prompt for password)
39+
bssh --password -H "user@host.com" "uptime"
40+
41+
# Use encrypted SSH key (will prompt for passphrase)
42+
bssh -i ~/.ssh/encrypted_key -c production "df -h"
43+
3844
# Limit parallel connections
3945
bssh -c production --parallel 5 "apt update"
4046
```

src/cli.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ pub struct Cli {
5757
)]
5858
pub use_agent: bool,
5959

60+
#[arg(
61+
short = 'P',
62+
long,
63+
help = "Use password authentication (will prompt for password)"
64+
)]
65+
pub password: bool,
66+
6067
#[arg(
6168
short = 'p',
6269
long,

src/executor.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pub struct ParallelExecutor {
2828
key_path: Option<String>,
2929
strict_mode: StrictHostKeyChecking,
3030
use_agent: bool,
31+
use_password: bool,
3132
}
3233

3334
impl ParallelExecutor {
@@ -52,6 +53,7 @@ impl ParallelExecutor {
5253
key_path,
5354
strict_mode,
5455
use_agent: false,
56+
use_password: false,
5557
}
5658
}
5759

@@ -68,6 +70,25 @@ impl ParallelExecutor {
6870
key_path,
6971
strict_mode,
7072
use_agent,
73+
use_password: false,
74+
}
75+
}
76+
77+
pub fn new_with_all_options(
78+
nodes: Vec<Node>,
79+
max_parallel: usize,
80+
key_path: Option<String>,
81+
strict_mode: StrictHostKeyChecking,
82+
use_agent: bool,
83+
use_password: bool,
84+
) -> Self {
85+
Self {
86+
nodes,
87+
max_parallel,
88+
key_path,
89+
strict_mode,
90+
use_agent,
91+
use_password,
7192
}
7293
}
7394

@@ -89,6 +110,7 @@ impl ParallelExecutor {
89110
let key_path = self.key_path.clone();
90111
let strict_mode = self.strict_mode;
91112
let use_agent = self.use_agent;
113+
let use_password = self.use_password;
92114
let semaphore = Arc::clone(&semaphore);
93115
let pb = multi_progress.add(ProgressBar::new_spinner());
94116
pb.set_style(style.clone());
@@ -107,6 +129,7 @@ impl ParallelExecutor {
107129
key_path.as_deref(),
108130
strict_mode,
109131
use_agent,
132+
use_password,
110133
)
111134
.await;
112135

@@ -170,6 +193,7 @@ impl ParallelExecutor {
170193
let key_path = self.key_path.clone();
171194
let strict_mode = self.strict_mode;
172195
let use_agent = self.use_agent;
196+
let use_password = self.use_password;
173197
let semaphore = Arc::clone(&semaphore);
174198
let pb = multi_progress.add(ProgressBar::new_spinner());
175199
pb.set_style(style.clone());
@@ -189,6 +213,7 @@ impl ParallelExecutor {
189213
key_path.as_deref(),
190214
strict_mode,
191215
use_agent,
216+
use_password,
192217
)
193218
.await;
194219

@@ -245,6 +270,7 @@ impl ParallelExecutor {
245270
let key_path = self.key_path.clone();
246271
let strict_mode = self.strict_mode;
247272
let use_agent = self.use_agent;
273+
let use_password = self.use_password;
248274
let semaphore = Arc::clone(&semaphore);
249275
let pb = multi_progress.add(ProgressBar::new_spinner());
250276
pb.set_style(style.clone());
@@ -276,6 +302,7 @@ impl ParallelExecutor {
276302
key_path.as_deref(),
277303
strict_mode,
278304
use_agent,
305+
use_password,
279306
)
280307
.await;
281308

@@ -338,6 +365,7 @@ impl ParallelExecutor {
338365
let key_path = self.key_path.clone();
339366
let strict_mode = self.strict_mode;
340367
let use_agent = self.use_agent;
368+
let use_password = self.use_password;
341369
let semaphore = Arc::clone(&semaphore);
342370
let pb = multi_progress.add(ProgressBar::new_spinner());
343371
pb.set_style(style.clone());
@@ -368,6 +396,7 @@ impl ParallelExecutor {
368396
key_path.as_deref(),
369397
strict_mode,
370398
use_agent,
399+
use_password,
371400
)
372401
.await;
373402

@@ -411,13 +440,20 @@ async fn execute_on_node(
411440
key_path: Option<&str>,
412441
strict_mode: StrictHostKeyChecking,
413442
use_agent: bool,
443+
use_password: bool,
414444
) -> Result<CommandResult> {
415445
let mut client = SshClient::new(node.host.clone(), node.port, node.username.clone());
416446

417447
let key_path = key_path.map(Path::new);
418448

419449
client
420-
.connect_and_execute_with_host_check(command, key_path, Some(strict_mode), use_agent)
450+
.connect_and_execute_with_host_check(
451+
command,
452+
key_path,
453+
Some(strict_mode),
454+
use_agent,
455+
use_password,
456+
)
421457
.await
422458
}
423459

@@ -428,6 +464,7 @@ async fn upload_to_node(
428464
key_path: Option<&str>,
429465
strict_mode: StrictHostKeyChecking,
430466
use_agent: bool,
467+
use_password: bool,
431468
) -> Result<()> {
432469
let mut client = SshClient::new(node.host.clone(), node.port, node.username.clone());
433470

@@ -442,6 +479,7 @@ async fn upload_to_node(
442479
key_path,
443480
Some(strict_mode),
444481
use_agent,
482+
use_password,
445483
)
446484
.await
447485
} else {
@@ -452,6 +490,7 @@ async fn upload_to_node(
452490
key_path,
453491
Some(strict_mode),
454492
use_agent,
493+
use_password,
455494
)
456495
.await
457496
}
@@ -464,6 +503,7 @@ async fn download_from_node(
464503
key_path: Option<&str>,
465504
strict_mode: StrictHostKeyChecking,
466505
use_agent: bool,
506+
use_password: bool,
467507
) -> Result<std::path::PathBuf> {
468508
let mut client = SshClient::new(node.host.clone(), node.port, node.username.clone());
469509

@@ -478,6 +518,7 @@ async fn download_from_node(
478518
key_path,
479519
Some(strict_mode),
480520
use_agent,
521+
use_password,
481522
)
482523
.await?;
483524

@@ -491,6 +532,7 @@ pub async fn download_dir_from_node(
491532
key_path: Option<&str>,
492533
strict_mode: StrictHostKeyChecking,
493534
use_agent: bool,
535+
use_password: bool,
494536
) -> Result<std::path::PathBuf> {
495537
let mut client = SshClient::new(node.host.clone(), node.port, node.username.clone());
496538

@@ -503,6 +545,7 @@ pub async fn download_dir_from_node(
503545
key_path,
504546
Some(strict_mode),
505547
use_agent,
548+
use_password,
506549
)
507550
.await?;
508551

src/main.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ struct ExecuteCommandParams<'a> {
3636
verbose: bool,
3737
strict_mode: StrictHostKeyChecking,
3838
use_agent: bool,
39+
use_password: bool,
3940
output_dir: Option<&'a Path>,
4041
}
4142

@@ -45,6 +46,7 @@ struct FileTransferParams<'a> {
4546
key_path: Option<&'a Path>,
4647
strict_mode: StrictHostKeyChecking,
4748
use_agent: bool,
49+
use_password: bool,
4850
recursive: bool,
4951
}
5052

@@ -96,6 +98,7 @@ async fn main() -> Result<()> {
9698
cli.identity.as_deref(),
9799
strict_mode,
98100
cli.use_agent,
101+
cli.password,
99102
)
100103
.await?;
101104
}
@@ -110,6 +113,7 @@ async fn main() -> Result<()> {
110113
key_path: cli.identity.as_deref(),
111114
strict_mode,
112115
use_agent: cli.use_agent,
116+
use_password: cli.password,
113117
recursive,
114118
};
115119
upload_file(params, &source, &destination).await?;
@@ -125,6 +129,7 @@ async fn main() -> Result<()> {
125129
key_path: cli.identity.as_deref(),
126130
strict_mode,
127131
use_agent: cli.use_agent,
132+
use_password: cli.password,
128133
recursive,
129134
};
130135
download_file(params, &source, &destination).await?;
@@ -139,6 +144,7 @@ async fn main() -> Result<()> {
139144
verbose: cli.verbose > 0,
140145
strict_mode,
141146
use_agent: cli.use_agent,
147+
use_password: cli.password,
142148
output_dir: cli.output_dir.as_deref(),
143149
};
144150
execute_command(params).await?;
@@ -207,16 +213,18 @@ async fn ping_nodes(
207213
key_path: Option<&Path>,
208214
strict_mode: StrictHostKeyChecking,
209215
use_agent: bool,
216+
use_password: bool,
210217
) -> Result<()> {
211218
println!("Pinging {} nodes...\n", nodes.len());
212219

213220
let key_path = key_path.map(|p| p.to_string_lossy().to_string());
214-
let executor = ParallelExecutor::new_with_strict_mode_and_agent(
221+
let executor = ParallelExecutor::new_with_all_options(
215222
nodes.clone(),
216223
max_parallel,
217224
key_path,
218225
strict_mode,
219226
use_agent,
227+
use_password,
220228
);
221229

222230
let results = executor.execute("echo 'pong'").await?;
@@ -250,12 +258,13 @@ async fn execute_command(params: ExecuteCommandParams<'_>) -> Result<()> {
250258
);
251259

252260
let key_path = params.key_path.map(|p| p.to_string_lossy().to_string());
253-
let executor = ParallelExecutor::new_with_strict_mode_and_agent(
261+
let executor = ParallelExecutor::new_with_all_options(
254262
params.nodes,
255263
params.max_parallel,
256264
key_path,
257265
params.strict_mode,
258266
params.use_agent,
267+
params.use_password,
259268
);
260269

261270
let results = executor.execute(params.command).await?;
@@ -475,12 +484,13 @@ async fn upload_file(
475484
println!("Destination: {destination}\n");
476485

477486
let key_path_str = params.key_path.map(|p| p.to_string_lossy().to_string());
478-
let executor = ParallelExecutor::new_with_strict_mode_and_agent(
487+
let executor = ParallelExecutor::new_with_all_options(
479488
params.nodes.clone(),
480489
params.max_parallel,
481490
key_path_str.clone(),
482491
params.strict_mode,
483492
params.use_agent,
493+
params.use_password,
484494
);
485495

486496
let mut total_success = 0;
@@ -686,12 +696,13 @@ async fn download_file(
686696
}
687697

688698
let key_path_str = params.key_path.map(|p| p.to_string_lossy().to_string());
689-
let executor = ParallelExecutor::new_with_strict_mode_and_agent(
699+
let executor = ParallelExecutor::new_with_all_options(
690700
params.nodes.clone(),
691701
params.max_parallel,
692702
key_path_str.clone(),
693703
params.strict_mode,
694704
params.use_agent,
705+
params.use_password,
695706
);
696707

697708
// Check if source contains glob pattern
@@ -735,6 +746,7 @@ async fn download_file(
735746
key_path_str.as_deref(),
736747
params.strict_mode,
737748
params.use_agent,
749+
params.use_password,
738750
)
739751
.await;
740752

@@ -783,6 +795,7 @@ async fn download_file(
783795
params.key_path,
784796
Some(params.strict_mode),
785797
params.use_agent,
798+
params.use_password,
786799
)
787800
.await?;
788801

0 commit comments

Comments
 (0)