mirror of
https://github.com/veeso/termscp.git
synced 2026-09-27 06:21:22 -07:00
feat(ssh): auto-fill ssh config parameters in auth form
Resolve SSH host parameters in auth forms and CLI connections while preserving explicit user, bookmark, and parsed alias values. Continue forwarding SSH config files for HostName and other SSH options, and document the precedence in English and Chinese. Closes #441
This commit is contained in:
@@ -243,6 +243,18 @@ enum FormTab {
|
||||
const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION";
|
||||
const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES";
|
||||
|
||||
fn should_resolve_ssh_host_params(
|
||||
protocol: FileTransferProtocol,
|
||||
mounted_address: &str,
|
||||
address: &str,
|
||||
force: bool,
|
||||
) -> bool {
|
||||
matches!(
|
||||
protocol,
|
||||
FileTransferProtocol::Scp | FileTransferProtocol::Sftp
|
||||
) && (force || mounted_address != address)
|
||||
}
|
||||
|
||||
/// AuthActivity is the data holder for the authentication activity
|
||||
pub struct AuthActivity {
|
||||
app: Application<Id, Msg, NoUserEvent>,
|
||||
@@ -256,7 +268,11 @@ pub struct AuthActivity {
|
||||
redraw: bool,
|
||||
/// Host bridge protocol
|
||||
host_bridge_protocol: HostBridgeProtocol,
|
||||
/// Last Host address applied to the Host Bridge form.
|
||||
last_host_bridge_address: String,
|
||||
last_form_tab: FormTab,
|
||||
/// Last Host address applied to the Remote form.
|
||||
last_remote_address: String,
|
||||
/// Remote file transfer protocol
|
||||
remote_protocol: FileTransferProtocol,
|
||||
context: Option<Context>,
|
||||
@@ -273,6 +289,8 @@ impl AuthActivity {
|
||||
bookmarks_list: Vec::new(),
|
||||
exit_reason: None,
|
||||
last_form_tab: FormTab::Remote,
|
||||
last_host_bridge_address: String::new(),
|
||||
last_remote_address: String::new(),
|
||||
recents_list: Vec::new(),
|
||||
redraw: true,
|
||||
host_bridge_protocol: HostBridgeProtocol::Localhost,
|
||||
@@ -317,6 +335,20 @@ impl AuthActivity {
|
||||
self.remote_protocol = protocol;
|
||||
}
|
||||
|
||||
fn last_mounted_address(&self, form_tab: FormTab) -> &str {
|
||||
match form_tab {
|
||||
FormTab::HostBridge => self.last_host_bridge_address.as_str(),
|
||||
FormTab::Remote => self.last_remote_address.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_last_mounted_address(&mut self, form_tab: FormTab, address: &str) {
|
||||
match form_tab {
|
||||
FormTab::HostBridge => self.last_host_bridge_address = address.to_string(),
|
||||
FormTab::Remote => self.last_remote_address = address.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current input mask to show
|
||||
fn host_bridge_input_mask(&self) -> InputMask {
|
||||
match self.host_bridge_protocol {
|
||||
@@ -444,4 +476,61 @@ mod tests {
|
||||
FileTransferProtocol::GoogleCloudStorage
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_resolve_ssh_params_only_after_host_change_or_forced_ssh_transition() {
|
||||
assert!(should_resolve_ssh_host_params(
|
||||
FileTransferProtocol::Sftp,
|
||||
"saved-host",
|
||||
"edited-host",
|
||||
false
|
||||
));
|
||||
assert!(!should_resolve_ssh_host_params(
|
||||
FileTransferProtocol::Sftp,
|
||||
"saved-host",
|
||||
"saved-host",
|
||||
false
|
||||
));
|
||||
assert!(should_resolve_ssh_host_params(
|
||||
FileTransferProtocol::Scp,
|
||||
"saved-host",
|
||||
"saved-host",
|
||||
true
|
||||
));
|
||||
assert!(!should_resolve_ssh_host_params(
|
||||
FileTransferProtocol::Ftp(false),
|
||||
"saved-host",
|
||||
"edited-host",
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_track_host_bridge_and_remote_addresses_independently() {
|
||||
let mut activity = AuthActivity::new(Duration::ZERO);
|
||||
|
||||
activity.set_last_mounted_address(FormTab::HostBridge, "bookmark-host");
|
||||
activity.set_last_mounted_address(FormTab::Remote, "recent-host");
|
||||
|
||||
assert_eq!(
|
||||
activity.last_mounted_address(FormTab::HostBridge),
|
||||
"bookmark-host"
|
||||
);
|
||||
assert_eq!(
|
||||
activity.last_mounted_address(FormTab::Remote),
|
||||
"recent-host"
|
||||
);
|
||||
assert!(!should_resolve_ssh_host_params(
|
||||
FileTransferProtocol::Sftp,
|
||||
activity.last_mounted_address(FormTab::Remote),
|
||||
"recent-host",
|
||||
false
|
||||
));
|
||||
assert!(should_resolve_ssh_host_params(
|
||||
FileTransferProtocol::Sftp,
|
||||
activity.last_mounted_address(FormTab::HostBridge),
|
||||
"edited-host",
|
||||
false
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ use tuirealm::state::{State, StateValue};
|
||||
|
||||
use super::{
|
||||
AuthActivity, AuthFormId, ExitReason, FormMsg, FormTab, HostBridgeProtocol, Id, InputMask, Msg,
|
||||
UiAuthFormMsg, UiMsg,
|
||||
UiAuthFormMsg, UiMsg, should_resolve_ssh_host_params,
|
||||
};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::utils::ssh::resolve_ssh_host_params;
|
||||
|
||||
impl AuthActivity {
|
||||
pub(super) fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
|
||||
@@ -24,6 +26,8 @@ impl AuthActivity {
|
||||
fn update_form(&mut self, msg: FormMsg) -> Option<Msg> {
|
||||
match msg {
|
||||
FormMsg::Connect => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::Remote, false);
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, false);
|
||||
let remote_params = match self.collect_remote_host_params() {
|
||||
Ok(remote_params) => remote_params,
|
||||
Err(err) => {
|
||||
@@ -142,24 +146,36 @@ impl AuthActivity {
|
||||
self.host_bridge_protocol = protocol;
|
||||
// Update port
|
||||
let port: u16 = self.get_input_port(FormTab::HostBridge);
|
||||
if let HostBridgeProtocol::Remote(remote_protocol) = protocol
|
||||
&& Self::is_port_standard(port)
|
||||
{
|
||||
self.mount_port(
|
||||
FormTab::HostBridge,
|
||||
Self::get_default_port_for_protocol(remote_protocol),
|
||||
);
|
||||
if let HostBridgeProtocol::Remote(remote_protocol) = protocol {
|
||||
match remote_protocol {
|
||||
FileTransferProtocol::Scp | FileTransferProtocol::Sftp => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, true);
|
||||
}
|
||||
_ if Self::is_port_standard(port) => {
|
||||
self.mount_port(
|
||||
FormTab::HostBridge,
|
||||
Self::get_default_port_for_protocol(remote_protocol),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
FormMsg::RemoteProtocolChanged(protocol) => {
|
||||
self.remote_protocol = protocol;
|
||||
// Update port
|
||||
let port: u16 = self.get_input_port(FormTab::Remote);
|
||||
if Self::is_port_standard(port) {
|
||||
self.mount_port(
|
||||
FormTab::Remote,
|
||||
Self::get_default_port_for_protocol(protocol),
|
||||
);
|
||||
match protocol {
|
||||
FileTransferProtocol::Scp | FileTransferProtocol::Sftp => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::Remote, true);
|
||||
}
|
||||
_ if Self::is_port_standard(port) => {
|
||||
self.mount_port(
|
||||
FormTab::Remote,
|
||||
Self::get_default_port_for_protocol(protocol),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
FormMsg::Quit => {
|
||||
@@ -253,6 +269,7 @@ impl AuthActivity {
|
||||
fn update_host_bridge_ui(&mut self, msg: UiAuthFormMsg) {
|
||||
match msg {
|
||||
UiAuthFormMsg::AddressBlurDown => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, false);
|
||||
let id = if cfg!(windows) && self.host_bridge_input_mask() == InputMask::Smb {
|
||||
Id::HostBridge(AuthFormId::SmbShare)
|
||||
} else {
|
||||
@@ -261,9 +278,11 @@ impl AuthActivity {
|
||||
self.activate_component(id);
|
||||
}
|
||||
UiAuthFormMsg::AddressBlurUp => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, false);
|
||||
self.activate_component(Id::HostBridge(AuthFormId::Protocol));
|
||||
}
|
||||
UiAuthFormMsg::ChangeFormTab => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, false);
|
||||
self.last_form_tab = FormTab::Remote;
|
||||
self.activate_component(Id::Remote(AuthFormId::Protocol));
|
||||
}
|
||||
@@ -278,6 +297,7 @@ impl AuthActivity {
|
||||
self.activate_component(id);
|
||||
}
|
||||
UiAuthFormMsg::ParamsFormBlur => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, false);
|
||||
self.activate_component(Id::BookmarksList);
|
||||
}
|
||||
UiAuthFormMsg::PasswordBlurDown => {
|
||||
@@ -496,6 +516,7 @@ impl AuthActivity {
|
||||
fn update_remote_ui(&mut self, msg: UiAuthFormMsg) {
|
||||
match msg {
|
||||
UiAuthFormMsg::AddressBlurDown => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::Remote, false);
|
||||
let id = if cfg!(windows) && self.remote_input_mask() == InputMask::Smb {
|
||||
Id::Remote(AuthFormId::SmbShare)
|
||||
} else {
|
||||
@@ -504,9 +525,11 @@ impl AuthActivity {
|
||||
self.activate_component(id);
|
||||
}
|
||||
UiAuthFormMsg::AddressBlurUp => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::Remote, false);
|
||||
self.activate_component(Id::Remote(AuthFormId::Protocol));
|
||||
}
|
||||
UiAuthFormMsg::ChangeFormTab => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::Remote, false);
|
||||
self.last_form_tab = FormTab::HostBridge;
|
||||
self.activate_component(Id::HostBridge(AuthFormId::Protocol));
|
||||
}
|
||||
@@ -517,6 +540,7 @@ impl AuthActivity {
|
||||
self.activate_component(Id::Remote(AuthFormId::RemoteDirectory));
|
||||
}
|
||||
UiAuthFormMsg::ParamsFormBlur => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::Remote, false);
|
||||
self.activate_component(Id::BookmarksList);
|
||||
}
|
||||
UiAuthFormMsg::PasswordBlurDown => {
|
||||
@@ -732,6 +756,35 @@ impl AuthActivity {
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_ssh_host_params_if_needed(&mut self, form_tab: FormTab, force: bool) {
|
||||
let protocol = match form_tab {
|
||||
FormTab::HostBridge => match self.host_bridge_protocol {
|
||||
HostBridgeProtocol::Localhost => return,
|
||||
HostBridgeProtocol::Remote(protocol) => protocol,
|
||||
},
|
||||
FormTab::Remote => self.remote_protocol,
|
||||
};
|
||||
let address = self.get_input_addr(form_tab);
|
||||
let should_resolve = should_resolve_ssh_host_params(
|
||||
protocol,
|
||||
self.last_mounted_address(form_tab),
|
||||
address.as_str(),
|
||||
force,
|
||||
);
|
||||
if !should_resolve {
|
||||
return;
|
||||
}
|
||||
|
||||
let params = resolve_ssh_host_params(self.context().ssh_config(), address.as_str());
|
||||
self.mount_port(form_tab, 22);
|
||||
self.mount_username(form_tab, "");
|
||||
self.mount_port(form_tab, params.port);
|
||||
if let Some(username) = params.username {
|
||||
self.mount_username(form_tab, username.as_str());
|
||||
}
|
||||
self.set_last_mounted_address(form_tab, address.as_str());
|
||||
}
|
||||
|
||||
fn activate_component(&mut self, id: Id) {
|
||||
if let Err(err) = self.app.active(&id) {
|
||||
error!("Failed to activate component: {err}");
|
||||
|
||||
@@ -320,6 +320,7 @@ impl AuthActivity {
|
||||
form_tab: FormTab,
|
||||
address: &str,
|
||||
) {
|
||||
self.set_last_mounted_address(form_tab, address);
|
||||
let addr_color = self.theme().auth_address;
|
||||
let id = Self::form_tab_id(form_tab, AuthFormId::Address);
|
||||
if let Err(err) = self.app.remount(
|
||||
|
||||
Reference in New Issue
Block a user