Files
termscp/src/utils/ssh.rs
T
Christian Visintin d99c76b43c 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
2026-09-01 12:52:10 +02:00

130 lines
4.1 KiB
Rust

//! ## SSH Utilities
//!
//! Provides small helpers for loading SSH configuration files used by bookmarks
//! and setup flows.
use ssh2_config::{ParseRule, SshConfig};
/// The standard port used when an SSH configuration does not define one.
const DEFAULT_SSH_PORT: u16 = 22;
/// Connection parameters resolved from an SSH host configuration.
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct SshHostParams {
/// Resolved SSH port.
pub(crate) port: u16,
/// Resolved SSH username, when configured.
pub(crate) username: Option<String>,
}
/// Parses an OpenSSH-style config file into an `ssh2_config::SshConfig`.
pub fn parse_ssh2_config(path: &str) -> Result<SshConfig, String> {
use std::fs::File;
use std::io::BufReader;
let mut reader = File::open(path)
.map_err(|e| format!("failed to open {path}: {e}"))
.map(BufReader::new)?;
SshConfig::default()
.parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)
.map_err(|e| format!("Failed to parse ssh2 config: {e}"))
}
/// Resolves SSH connection parameters for a host from the startup-parsed configuration.
pub(crate) fn resolve_ssh_host_params(config: Option<&SshConfig>, host: &str) -> SshHostParams {
let params = config.map(|config| config.query(host));
SshHostParams {
port: params
.as_ref()
.and_then(|params| params.port)
.unwrap_or(DEFAULT_SSH_PORT),
username: params.and_then(|params| params.user),
}
}
#[cfg(test)]
mod test {
use super::{SshHostParams, parse_ssh2_config, resolve_ssh_host_params};
use crate::utils::test_helpers;
#[test]
fn should_parse_ssh2_config() {
let rsa_key = test_helpers::create_sample_file_with_content(
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a",
);
let ssh_config_file = test_helpers::create_sample_file_with_content(format!(
r#"
Host test
HostName 127.0.0.1
Port 2222
User test
IdentityFile {}
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
"#,
rsa_key.path().display()
));
assert!(
parse_ssh2_config(
ssh_config_file
.path()
.to_string_lossy()
.to_string()
.as_str()
)
.is_ok()
);
}
#[test]
fn ssh_host_params_should_resolve_exact_and_wildcard_hosts() {
let ssh_config_file = test_helpers::create_sample_file_with_content(
r#"
Host exact-host
Port 2222
User exact-user
Host *.example.com
Port 2200
User wildcard-user
"#,
);
let config = parse_ssh2_config(&ssh_config_file.path().to_string_lossy())
.expect("test SSH configuration should parse");
assert_eq!(
resolve_ssh_host_params(Some(&config), "exact-host"),
SshHostParams {
port: 2222,
username: Some("exact-user".to_string()),
}
);
assert_eq!(
resolve_ssh_host_params(Some(&config), "server.example.com"),
SshHostParams {
port: 2200,
username: Some("wildcard-user".to_string()),
}
);
}
#[test]
fn ssh_host_params_should_default_when_configuration_has_no_values() {
let ssh_config_file =
test_helpers::create_sample_file_with_content("Host unconfigured-host\n");
let config = parse_ssh2_config(&ssh_config_file.path().to_string_lossy())
.expect("test SSH configuration should parse");
assert_eq!(
resolve_ssh_host_params(Some(&config), "unconfigured-host"),
SshHostParams {
port: 22,
username: None,
}
);
}
}