put: expand ~ in ssh_config IdentityFile paths

IdentityFile values were stored verbatim, so the near-universal
'IdentityFile ~/.ssh/key' spelling never matched an existing file and
the key was silently skipped during authentication. Expand a leading
~ (only that form; ~user and embedded tildes stay verbatim) against
the user's home directory when parsing.
This commit is contained in:
2026-09-17 16:31:04 +02:00
parent 775e3d3b8a
commit 57db98d776
+62 -5
View File
@@ -120,7 +120,7 @@ fn apply_config_file(config: &mut SshConfig, content: &str, host: &str) {
config.port = Some(port); config.port = Some(port);
} }
} else if keyword.eq_ignore_ascii_case("identityfile") { } else if keyword.eq_ignore_ascii_case("identityfile") {
let path = PathBuf::from(rest); let path = expand_tilde(rest);
if !config.identity_files.contains(&path) { if !config.identity_files.contains(&path) {
config.identity_files.push(path); config.identity_files.push(path);
} }
@@ -128,6 +128,37 @@ fn apply_config_file(config: &mut SshConfig, content: &str, host: &str) {
} }
} }
/// The current user's home directory: the `directories` crate first (which
/// consults `$HOME` on Unix), with a direct `$HOME` fallback.
fn current_home() -> Option<PathBuf> {
directories::UserDirs::new()
.map(|dirs| dirs.home_dir().to_path_buf())
.or_else(|| std::env::var_os("HOME").map(PathBuf::from))
}
/// Expand a leading `~` or `~/` in `path` against `home`: the near-universal
/// `IdentityFile ~/.ssh/key` spelling. Only a leading tilde is handled —
/// `~user/...` (another user's home) and tildes appearing anywhere else are
/// kept verbatim — and without a known home directory the path is returned
/// unchanged.
fn expand_tilde_with(path: &str, home: Option<&Path>) -> PathBuf {
let Some(home) = home else {
return PathBuf::from(path);
};
if path == "~" {
home.to_path_buf()
} else if let Some(rest) = path.strip_prefix("~/") {
home.join(rest)
} else {
PathBuf::from(path)
}
}
/// [`expand_tilde_with`] against the current user's home directory
fn expand_tilde(path: &str) -> PathBuf {
expand_tilde_with(path, current_home().as_deref())
}
/// One `Host` block pattern against a host name: exact match, `*` (any /// One `Host` block pattern against a host name: exact match, `*` (any
/// run), `?` (one character) and `!pattern` negation (a matching negated /// run), `?` (one character) and `!pattern` negation (a matching negated
/// pattern excludes the host from the block). /// pattern excludes the host from the block).
@@ -557,17 +588,43 @@ Host *
); );
assert_eq!(config.user.as_deref(), Some("myuser")); assert_eq!(config.user.as_deref(), Some("myuser"));
// A leading tilde is expanded to the home directory, built from the
// same lookup the helper uses so no specific username is assumed
let home = current_home().expect("tests require a home directory");
assert_eq!( assert_eq!(
config.identity_files, config.identity_files,
vec![ vec![home.join(".ssh/lp_key"), home.join(".ssh/other_key")]
PathBuf::from("~/.ssh/lp_key"),
PathBuf::from("~/.ssh/other_key")
]
); );
assert_eq!(config.host_name, None); assert_eq!(config.host_name, None);
assert_eq!(config.port, None); assert_eq!(config.port, None);
} }
/// Only a leading `~`/`~/` expands to the home directory; `~user/...`
/// and non-tilde paths are kept verbatim, and nothing is expanded
/// without a known home directory
#[test]
fn expand_tilde_handles_leading_tilde_only() {
let home = Some(Path::new("/home/testuser"));
assert_eq!(
expand_tilde_with("~/x", home),
PathBuf::from("/home/testuser/x")
);
assert_eq!(
expand_tilde_with("~", home),
PathBuf::from("/home/testuser")
);
assert_eq!(
expand_tilde_with("~other/x", home),
PathBuf::from("~other/x")
);
assert_eq!(
expand_tilde_with("relative", home),
PathBuf::from("relative")
);
assert_eq!(expand_tilde_with("/abs", home), PathBuf::from("/abs"));
assert_eq!(expand_tilde_with("~/x", None), PathBuf::from("~/x"));
}
#[test] #[test]
fn config_wildcard_and_question_marks_match() { fn config_wildcard_and_question_marks_match() {
let config = apply_config_file_all( let config = apply_config_file_all(