use std::collections::HashSet;
use std::ffi::OsString;
use std::fs;
use std::io::Write;
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use second_brain_core::machine::MachineIdentity;
pub fn known_hosts_path_in(config_dir: &Path) -> PathBuf {
config_dir.join("known_hosts")
}
pub fn known_hosts_path() -> Result<PathBuf> {
Ok(known_hosts_path_in(&MachineIdentity::config_dir()?))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyscanLine {
pub host: String,
pub key_type: String,
pub public_key: String,
}
pub fn parse_keyscan_output(raw: &str) -> Vec<KeyscanLine> {
raw.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
return None;
}
let mut parts = line.split_whitespace();
let host = parts.next()?;
let key_type = parts.next()?;
let public_key = parts.next()?;
Some(KeyscanLine {
host: host.to_string(),
key_type: key_type.to_string(),
public_key: public_key.to_string(),
})
})
.collect()
}
pub fn compose_known_host_entry(line: &KeyscanLine) -> String {
format!("{} {} {}\n", line.host, line.key_type, line.public_key)
}
pub fn append_known_host(path: &Path, lines: &[KeyscanLine]) -> Result<()> {
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
fs::create_dir_all(parent).context("creating known_hosts parent dir")?;
}
let existing = match fs::read_to_string(path) {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
Err(e) => return Err(e).context("reading known_hosts"),
};
let already: HashSet<&str> = existing
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.collect();
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.mode(0o600)
.open(path)
.context("opening known_hosts for append")?;
for line in lines {
let entry = compose_known_host_entry(line);
if already.contains(entry.trim_end()) {
continue;
}
file.write_all(entry.as_bytes())
.context("writing known_hosts entry")?;
}
fs::set_permissions(path, fs::Permissions::from_mode(0o600))
.context("setting known_hosts mode 0600")?;
Ok(())
}
pub fn untrusted_host_hint(host: &str) -> String {
format!(
"if ssh reported a host-key error, the host is not in ~/.second-brain/known_hosts yet — \
run `sb sync trust {host}` to verify and pin its fingerprint."
)
}
pub fn ssh_args(known_hosts: &Path) -> Vec<OsString> {
let mut user_kh = OsString::from("UserKnownHostsFile=");
user_kh.push(known_hosts);
vec![
OsString::from("-o"),
OsString::from("StrictHostKeyChecking=yes"),
OsString::from("-o"),
user_kh,
OsString::from("-o"),
OsString::from("GlobalKnownHostsFile=/dev/null"),
OsString::from("-o"),
OsString::from("ServerAliveInterval=30"),
]
}