mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-09-22 08:01:00 +00:00
feat: support keyboard-interactive auth and interactive passphrase prompts (resolves #33)
This commit is contained in:
@@ -179,3 +179,4 @@ sync_downloading: "Downloading and decrypting configuration..."
|
||||
sync_upload_complete: "Configuration uploaded"
|
||||
sync_download_complete: "Configuration downloaded"
|
||||
sync_failed: "Synchronization failed"
|
||||
keyboard_interactive: "Keyboard Interactive"
|
||||
|
||||
@@ -181,3 +181,4 @@ sync_downloading: "正在下载并解密配置..."
|
||||
sync_upload_complete: "配置上传完成"
|
||||
sync_download_complete: "配置下载完成"
|
||||
sync_failed: "同步失败"
|
||||
keyboard_interactive: "键盘交互"
|
||||
|
||||
+124
-10
@@ -1,5 +1,5 @@
|
||||
use gpui::{
|
||||
Anchor, Context, Focusable as _, FontWeight, InteractiveElement as _, MouseButton,
|
||||
Anchor, AppContext as _, Context, Focusable as _, FontWeight, InteractiveElement as _, MouseButton,
|
||||
ParentElement as _, SharedString, StatefulInteractiveElement as _, Styled as _, Window, div,
|
||||
prelude::FluentBuilder as _, px, rems,
|
||||
};
|
||||
@@ -8,7 +8,7 @@ use gpui_component::{
|
||||
button::{Button, ButtonVariants as _},
|
||||
dialog::Dialog,
|
||||
h_flex,
|
||||
input::Input,
|
||||
input::{Input, InputState},
|
||||
menu::{DropdownMenu as _, PopupMenuItem},
|
||||
progress::Progress,
|
||||
scroll::{Scrollbar, ScrollbarShow},
|
||||
@@ -62,7 +62,10 @@ impl Ashell {
|
||||
let key_inline_input = key_inline_input.clone();
|
||||
let passphrase_input = passphrase_input.clone();
|
||||
move |content, window, cx| {
|
||||
let is_password = view.read(cx).ssh_auth_method == AuthMethod::Password;
|
||||
let method = view.read(cx).ssh_auth_method;
|
||||
let is_password = method == AuthMethod::Password;
|
||||
let is_key = method == AuthMethod::Key;
|
||||
let is_kb = method == AuthMethod::KeyboardInteractive;
|
||||
let is_editing = view.read(cx).editing_session_id.is_some();
|
||||
content.child(
|
||||
v_flex()
|
||||
@@ -89,13 +92,13 @@ impl Ashell {
|
||||
AuthMethod::Password,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)),
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Button::new("ssh-auth-key")
|
||||
.label(t!("key").to_string())
|
||||
.when(!is_password, |button| button.primary())
|
||||
.when(is_key, |button| button.primary())
|
||||
.on_click(window.listener_for(
|
||||
&view,
|
||||
|this, _, _, cx| {
|
||||
@@ -103,8 +106,22 @@ impl Ashell {
|
||||
AuthMethod::Key,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)),
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Button::new("ssh-auth-kb")
|
||||
.label(t!("keyboard_interactive").to_string())
|
||||
.when(is_kb, |button| button.primary())
|
||||
.on_click(window.listener_for(
|
||||
&view,
|
||||
|this, _, _, cx| {
|
||||
this.set_ssh_auth_method(
|
||||
AuthMethod::KeyboardInteractive,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)),
|
||||
),
|
||||
)
|
||||
.when(is_password, |this| {
|
||||
@@ -112,7 +129,7 @@ impl Ashell {
|
||||
Input::new(&password_input).mask_toggle().tab_index(4),
|
||||
)
|
||||
})
|
||||
.when(!is_password, |this| {
|
||||
.when(is_key, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
@@ -1761,7 +1778,7 @@ impl Ashell {
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(rems(0.9))
|
||||
.text_size(rems(0.9))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(t!("about_feedback_hint")),
|
||||
)
|
||||
@@ -1782,4 +1799,101 @@ impl Ashell {
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn show_interactive_prompt_dialog(
|
||||
&mut self,
|
||||
tab_id: String,
|
||||
prompt_type: crate::terminal::PromptType,
|
||||
instruction: String,
|
||||
prompts: Vec<crate::terminal::PromptInfo>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let view = cx.entity();
|
||||
|
||||
// Dynamically instantiate InputState for each prompt
|
||||
let mut input_states = Vec::new();
|
||||
for p in &prompts {
|
||||
let is_masked = !p.echo;
|
||||
let input_state = cx.new(|cx| {
|
||||
let mut state = InputState::new(window, cx).placeholder(p.prompt.clone());
|
||||
if is_masked {
|
||||
state = state.masked(true);
|
||||
}
|
||||
state
|
||||
});
|
||||
input_states.push(input_state);
|
||||
}
|
||||
|
||||
let tab_id_clone = tab_id.clone();
|
||||
let input_states_clone = input_states.clone();
|
||||
|
||||
self.active_dialog = Some(crate::app::DialogKind::PromptRequest);
|
||||
|
||||
let tab_id_for_close = tab_id.clone();
|
||||
|
||||
window.open_dialog(cx, move |dialog: Dialog, _window, _| {
|
||||
let title = match prompt_type {
|
||||
crate::terminal::PromptType::KeyboardInteractive => "Keyboard Interactive Authentication",
|
||||
crate::terminal::PromptType::Passphrase => "Enter Private Key Passphrase",
|
||||
};
|
||||
|
||||
let tab_id_for_ok = tab_id_clone.clone();
|
||||
let input_states_for_ok = input_states_clone.clone();
|
||||
let view_for_ok = view.clone();
|
||||
|
||||
let view_for_close = view.clone();
|
||||
let tab_id_for_close = tab_id_for_close.clone();
|
||||
|
||||
let instruction_for_content = instruction.clone();
|
||||
let input_states_for_content = input_states_clone.clone();
|
||||
|
||||
dialog
|
||||
.title(title)
|
||||
.w(px(500.))
|
||||
.overlay_closable(false)
|
||||
.on_close(move |_, _, cx| {
|
||||
view_for_close.update(cx, |this, cx| {
|
||||
this.active_dialog = None;
|
||||
// Send Close command to abort connection if they close the dialog without OK
|
||||
if let Some(tab) = this.tabs.iter().find(|t| t.id == tab_id_for_close) {
|
||||
tab.backend.send(crate::terminal::BackendCommand::Close);
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.on_ok(move |_, window, cx| {
|
||||
view_for_ok.update(cx, |this, cx| {
|
||||
this.active_dialog = None;
|
||||
let mut responses = Vec::new();
|
||||
for state in &input_states_for_ok {
|
||||
responses.push(state.read(cx).text().to_string());
|
||||
}
|
||||
if let Some(tab) = this.tabs.iter().find(|t| t.id == tab_id_for_ok) {
|
||||
tab.backend.send(crate::terminal::BackendCommand::PromptResponse(responses));
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
window.close_dialog(cx);
|
||||
true
|
||||
})
|
||||
.content(move |content, _window, _cx| {
|
||||
let mut container = v_flex().gap_3();
|
||||
if !instruction_for_content.is_empty() {
|
||||
container = container.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(gpui::rgba(0x808080ff))
|
||||
.child(instruction_for_content.clone())
|
||||
);
|
||||
}
|
||||
for input_state in &input_states_for_content {
|
||||
container = container.child(
|
||||
Input::new(input_state).w_full()
|
||||
);
|
||||
}
|
||||
content.child(container)
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+29
-11
@@ -196,6 +196,7 @@ pub(crate) enum DialogKind {
|
||||
SessionSelector,
|
||||
Transfers,
|
||||
NewSsh,
|
||||
PromptRequest,
|
||||
}
|
||||
|
||||
pub(crate) struct Ashell {
|
||||
@@ -604,7 +605,7 @@ impl Ashell {
|
||||
|
||||
this.apply_theme_preferences(window, cx);
|
||||
// this.open_local(cx);
|
||||
this.start_event_pump(cx);
|
||||
this.start_event_pump(window, cx);
|
||||
this
|
||||
}
|
||||
|
||||
@@ -654,17 +655,19 @@ impl Ashell {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn start_event_pump(&self, cx: &mut Context<Self>) {
|
||||
cx.spawn(async move |this, cx| {
|
||||
pub(crate) fn start_event_pump(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.spawn_in(window, async move |this, mut cx| {
|
||||
let mut idle_frames = 0u32;
|
||||
loop {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(16))
|
||||
.await;
|
||||
if this
|
||||
.update(cx, |this, cx| {
|
||||
let changed = this.drain_backend_events();
|
||||
let system_sampled = this.sample_system_if_due();
|
||||
let mut changed = false;
|
||||
let mut system_sampled = false;
|
||||
let update_res = gpui::AsyncWindowContext::update(&mut cx, |window, cx| {
|
||||
let _ = this.update(cx, |this, cx| {
|
||||
changed = this.drain_backend_events(window, cx);
|
||||
system_sampled = this.sample_system_if_due();
|
||||
this.sync_theme_if_due(cx);
|
||||
if changed || system_sampled {
|
||||
cx.notify();
|
||||
@@ -676,9 +679,9 @@ impl Ashell {
|
||||
idle_frames = 0;
|
||||
}
|
||||
}
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
});
|
||||
});
|
||||
if update_res.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -686,7 +689,7 @@ impl Ashell {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub(crate) fn drain_backend_events(&mut self) -> bool {
|
||||
pub(crate) fn drain_backend_events(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
||||
let mut changed = false;
|
||||
let mut transfers_changed = false;
|
||||
while let Ok(event) = self.events_rx.try_recv() {
|
||||
@@ -725,6 +728,21 @@ impl Ashell {
|
||||
self.connection_progress = None;
|
||||
}
|
||||
}
|
||||
BackendEvent::PromptRequest {
|
||||
tab_id,
|
||||
prompt_type,
|
||||
instruction,
|
||||
prompts,
|
||||
} => {
|
||||
self.show_interactive_prompt_dialog(
|
||||
tab_id,
|
||||
prompt_type,
|
||||
instruction,
|
||||
prompts,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
BackendEvent::SftpEntries {
|
||||
tab_id,
|
||||
path,
|
||||
|
||||
@@ -117,6 +117,7 @@ pub fn spawn_local_terminal(
|
||||
}
|
||||
BackendCommand::Close => break,
|
||||
BackendCommand::SampleMetrics => {}
|
||||
BackendCommand::PromptResponse(_) => {}
|
||||
},
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||
if let Ok(Some(status)) = child.try_wait() {
|
||||
|
||||
+167
-3
@@ -16,8 +16,23 @@ use tokio::sync::mpsc;
|
||||
use crate::{
|
||||
session::config::{AuthMethod, Session},
|
||||
system::{SystemSnapshot, remote_snapshot_from_kv},
|
||||
terminal::{BackendCommand, BackendEvent, BackendTx},
|
||||
terminal::{BackendCommand, BackendEvent, BackendTx, PromptType, PromptInfo},
|
||||
};
|
||||
use std::sync::OnceLock;
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedCreds {
|
||||
pub password: Option<String>,
|
||||
pub passphrase: Option<String>,
|
||||
pub kb_responses: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub static CREDENTIALS_CACHE: OnceLock<std::sync::Mutex<HashMap<String, CachedCreds>>> = OnceLock::new();
|
||||
|
||||
pub static PROMPT_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
|
||||
pub fn spawn_ssh_terminal(
|
||||
runtime: &tokio::runtime::Handle,
|
||||
@@ -95,7 +110,7 @@ async fn run_ssh(
|
||||
});
|
||||
|
||||
let handle = Arc::new(tokio::sync::Mutex::new(
|
||||
connect_and_authenticate(&tab_id, &session, &events).await?,
|
||||
connect_and_authenticate(&tab_id, &session, &events, &mut commands).await?,
|
||||
));
|
||||
|
||||
let mut channel = handle
|
||||
@@ -156,6 +171,9 @@ async fn run_ssh(
|
||||
}
|
||||
});
|
||||
}
|
||||
Some(BackendCommand::PromptResponse(_)) => {
|
||||
tracing::warn!("[ssh] received unexpected prompt response after authentication");
|
||||
}
|
||||
Some(BackendCommand::Close) | None => {
|
||||
tracing::info!("[ssh] local client closed the session for tab {}", tab_id);
|
||||
let _ = channel.eof().await;
|
||||
@@ -213,10 +231,27 @@ async fn run_ssh(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_session_private_key_with_cache(session: &Session) -> Result<PrivateKey> {
|
||||
let cached_passphrase = {
|
||||
let cache_lock = CREDENTIALS_CACHE.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
|
||||
let cache = cache_lock.lock().unwrap();
|
||||
cache.get(&session.id).and_then(|c| c.passphrase.clone())
|
||||
};
|
||||
if let Some(p) = cached_passphrase {
|
||||
let mut temp_session = session.clone();
|
||||
temp_session.passphrase = p;
|
||||
if let Ok(key) = load_session_private_key(&temp_session) {
|
||||
return Ok(key);
|
||||
}
|
||||
}
|
||||
load_session_private_key(session)
|
||||
}
|
||||
|
||||
async fn connect_and_authenticate(
|
||||
tab_id: &str,
|
||||
session: &Session,
|
||||
events: &std::sync::mpsc::Sender<BackendEvent>,
|
||||
commands: &mut mpsc::UnboundedReceiver<BackendCommand>,
|
||||
) -> Result<russh::client::Handle<ClientHandler>> {
|
||||
let config = Arc::new(client::Config {
|
||||
inactivity_timeout: Some(std::time::Duration::from_secs(600)),
|
||||
@@ -271,7 +306,55 @@ async fn connect_and_authenticate(
|
||||
tab_id: tab_id.to_string(),
|
||||
text: format!("connected to {addr}, loading private key from {source}"),
|
||||
});
|
||||
let keypair = load_session_private_key(session)?;
|
||||
let mut keypair = load_session_private_key_with_cache(session).await;
|
||||
if let Err(e) = &keypair {
|
||||
let err_str = e.to_string();
|
||||
if err_str.contains("encrypted") || err_str.contains("passphrase") || err_str.contains("decrypt") {
|
||||
let prompt_lock = PROMPT_LOCK.get_or_init(|| Mutex::new(()));
|
||||
let _guard = prompt_lock.lock().await;
|
||||
let _ = events.send(BackendEvent::PromptRequest {
|
||||
tab_id: tab_id.to_string(),
|
||||
prompt_type: PromptType::Passphrase,
|
||||
instruction: format!("Enter passphrase for private key (source: {})", source),
|
||||
prompts: vec![PromptInfo {
|
||||
prompt: "Passphrase".to_string(),
|
||||
echo: false,
|
||||
}],
|
||||
});
|
||||
let mut passphrase_res = None;
|
||||
while let Some(cmd) = commands.recv().await {
|
||||
match cmd {
|
||||
BackendCommand::PromptResponse(responses) => {
|
||||
if let Some(p) = responses.first().cloned() {
|
||||
passphrase_res = Some(p);
|
||||
}
|
||||
break;
|
||||
}
|
||||
BackendCommand::Close => {
|
||||
return Err(anyhow!("Authentication cancelled by user"));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(p) = passphrase_res {
|
||||
{
|
||||
let cache_lock = CREDENTIALS_CACHE.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
|
||||
let mut cache = cache_lock.lock().unwrap();
|
||||
cache.entry(session.id.clone()).or_insert_with(|| CachedCreds {
|
||||
password: None,
|
||||
passphrase: None,
|
||||
kb_responses: None,
|
||||
}).passphrase = Some(p.clone());
|
||||
}
|
||||
let mut temp_session = session.clone();
|
||||
temp_session.passphrase = p;
|
||||
keypair = load_session_private_key(&temp_session);
|
||||
} else {
|
||||
return Err(anyhow!("Passphrase prompt cancelled"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let keypair = keypair.context("failed to load private key")?;
|
||||
let algorithm = format!("{:?}", keypair.algorithm());
|
||||
let _ = events.send(BackendEvent::Status {
|
||||
tab_id: tab_id.to_string(),
|
||||
@@ -307,6 +390,83 @@ async fn connect_and_authenticate(
|
||||
}
|
||||
success
|
||||
}
|
||||
AuthMethod::KeyboardInteractive => {
|
||||
let cached = {
|
||||
let cache_lock = CREDENTIALS_CACHE.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
|
||||
let cache = cache_lock.lock().unwrap();
|
||||
cache.get(&session.id).cloned()
|
||||
};
|
||||
let mut response = if cached.as_ref().and_then(|c| c.kb_responses.as_ref()).is_some() {
|
||||
handle.authenticate_keyboard_interactive_start(&session.user, None).await?
|
||||
} else {
|
||||
let prompt_lock = PROMPT_LOCK.get_or_init(|| Mutex::new(()));
|
||||
let _guard = prompt_lock.lock().await;
|
||||
handle.authenticate_keyboard_interactive_start(&session.user, None).await?
|
||||
};
|
||||
loop {
|
||||
match response {
|
||||
russh::client::KeyboardInteractiveAuthResponse::Success => {
|
||||
break true;
|
||||
}
|
||||
russh::client::KeyboardInteractiveAuthResponse::Failure => {
|
||||
break false;
|
||||
}
|
||||
russh::client::KeyboardInteractiveAuthResponse::InfoRequest { name, instructions, prompts } => {
|
||||
let mut responses = Vec::new();
|
||||
let mut cache_hit = false;
|
||||
if let Some(c) = cached.as_ref().and_then(|c| c.kb_responses.as_ref()) {
|
||||
if c.len() == prompts.len() {
|
||||
responses = c.clone();
|
||||
cache_hit = true;
|
||||
}
|
||||
}
|
||||
if !cache_hit {
|
||||
let prompt_lock = PROMPT_LOCK.get_or_init(|| Mutex::new(()));
|
||||
let _guard = prompt_lock.lock().await;
|
||||
let mut prompt_infos = Vec::new();
|
||||
for p in &prompts {
|
||||
prompt_infos.push(PromptInfo {
|
||||
prompt: p.prompt.clone(),
|
||||
echo: p.echo,
|
||||
});
|
||||
}
|
||||
let _ = events.send(BackendEvent::PromptRequest {
|
||||
tab_id: tab_id.to_string(),
|
||||
prompt_type: PromptType::KeyboardInteractive,
|
||||
instruction: format!("{}\n{}", name, instructions),
|
||||
prompts: prompt_infos,
|
||||
});
|
||||
let mut prompt_res = None;
|
||||
while let Some(cmd) = commands.recv().await {
|
||||
match cmd {
|
||||
BackendCommand::PromptResponse(res) => {
|
||||
prompt_res = Some(res);
|
||||
break;
|
||||
}
|
||||
BackendCommand::Close => {
|
||||
return Err(anyhow!("Authentication cancelled"));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(res) = prompt_res {
|
||||
responses = res;
|
||||
let cache_lock = CREDENTIALS_CACHE.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
|
||||
let mut cache = cache_lock.lock().unwrap();
|
||||
cache.entry(session.id.clone()).or_insert_with(|| CachedCreds {
|
||||
password: None,
|
||||
passphrase: None,
|
||||
kb_responses: None,
|
||||
}).kb_responses = Some(responses.clone());
|
||||
} else {
|
||||
return Err(anyhow!("Keyboard-interactive cancelled"));
|
||||
}
|
||||
}
|
||||
response = handle.authenticate_keyboard_interactive_respond(responses).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !authed {
|
||||
@@ -328,6 +488,10 @@ async fn connect_and_authenticate(
|
||||
session.port,
|
||||
key_source_label(session)
|
||||
),
|
||||
AuthMethod::KeyboardInteractive => format!(
|
||||
"authentication failed: server rejected keyboard-interactive authentication for {}@{}:{}",
|
||||
session.user, session.host, session.port
|
||||
),
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
+23
-1
@@ -6,10 +6,11 @@ use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum AuthMethod {
|
||||
Password,
|
||||
Key,
|
||||
KeyboardInteractive,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -73,6 +74,27 @@ impl Session {
|
||||
last_used: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keyboard_interactive(
|
||||
host: String,
|
||||
port: u16,
|
||||
user: String,
|
||||
) -> Self {
|
||||
let name = format!("{user}@{host}");
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
name,
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
auth: AuthMethod::KeyboardInteractive,
|
||||
password: String::new(),
|
||||
private_key_path: String::new(),
|
||||
private_key_inline: String::new(),
|
||||
passphrase: String::new(),
|
||||
last_used: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -100,6 +100,7 @@ impl Ashell {
|
||||
}
|
||||
Session::key(host, port, user, key_path, key_inline, passphrase)
|
||||
}
|
||||
AuthMethod::KeyboardInteractive => Session::keyboard_interactive(host, port, user),
|
||||
};
|
||||
session.name = name;
|
||||
if let Some(id) = existing_id {
|
||||
|
||||
+40
-2
@@ -868,6 +868,34 @@ async fn connect_and_authenticate(
|
||||
}
|
||||
success
|
||||
}
|
||||
AuthMethod::KeyboardInteractive => {
|
||||
let cached_kb_responses = {
|
||||
let cache_lock = crate::backend::ssh::CREDENTIALS_CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
|
||||
let cache = cache_lock.lock().unwrap();
|
||||
cache.get(&session.id).and_then(|c| c.kb_responses.clone())
|
||||
};
|
||||
let responses = cached_kb_responses.unwrap_or_else(|| vec![session.password.clone()]);
|
||||
let mut response = handle.authenticate_keyboard_interactive_start(&session.user, None).await?;
|
||||
loop {
|
||||
match response {
|
||||
russh::client::KeyboardInteractiveAuthResponse::Success => {
|
||||
break true;
|
||||
}
|
||||
russh::client::KeyboardInteractiveAuthResponse::Failure => {
|
||||
break false;
|
||||
}
|
||||
russh::client::KeyboardInteractiveAuthResponse::InfoRequest { prompts, .. } => {
|
||||
let mut resp = responses.clone();
|
||||
if resp.len() < prompts.len() {
|
||||
resp.resize(prompts.len(), String::new());
|
||||
} else if resp.len() > prompts.len() {
|
||||
resp.truncate(prompts.len());
|
||||
}
|
||||
response = handle.authenticate_keyboard_interactive_respond(resp).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !authed {
|
||||
@@ -879,6 +907,7 @@ async fn connect_and_authenticate(
|
||||
match session.auth {
|
||||
AuthMethod::Password => "password",
|
||||
AuthMethod::Key => "public key",
|
||||
AuthMethod::KeyboardInteractive => "keyboard interactive",
|
||||
},
|
||||
session.user,
|
||||
session.host,
|
||||
@@ -892,8 +921,17 @@ async fn connect_and_authenticate(
|
||||
fn load_session_private_key(session: &Session) -> Result<PrivateKey> {
|
||||
let inline_key = normalize_inline_private_key(&session.private_key_inline);
|
||||
let key_path = expand_key_path(session.private_key_path.trim());
|
||||
let passphrase = session.passphrase.trim();
|
||||
let passphrase = (!passphrase.is_empty()).then_some(passphrase);
|
||||
let cached_passphrase = {
|
||||
let cache_lock = crate::backend::ssh::CREDENTIALS_CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
|
||||
let cache = cache_lock.lock().unwrap();
|
||||
cache.get(&session.id).and_then(|c| c.passphrase.clone())
|
||||
};
|
||||
let passphrase = if let Some(ref p) = cached_passphrase {
|
||||
Some(p.as_str())
|
||||
} else {
|
||||
let p = session.passphrase.trim();
|
||||
(!p.is_empty()).then_some(p)
|
||||
};
|
||||
let has_inline = !inline_key.is_empty();
|
||||
let has_path = key_path.is_some();
|
||||
|
||||
|
||||
@@ -24,16 +24,35 @@ pub enum TabKind {
|
||||
Ssh,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PromptType {
|
||||
KeyboardInteractive,
|
||||
Passphrase,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PromptInfo {
|
||||
pub prompt: String,
|
||||
pub echo: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BackendCommand {
|
||||
Input(Vec<u8>),
|
||||
Resize { cols: u16, rows: u16 },
|
||||
SampleMetrics,
|
||||
Close,
|
||||
PromptResponse(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BackendEvent {
|
||||
PromptRequest {
|
||||
tab_id: String,
|
||||
prompt_type: PromptType,
|
||||
instruction: String,
|
||||
prompts: Vec<PromptInfo>,
|
||||
},
|
||||
Output {
|
||||
tab_id: String,
|
||||
bytes: Vec<u8>,
|
||||
|
||||
Reference in New Issue
Block a user