hardpass-vm 0.3.1

A small, reliable Ubuntu cloud-image VM manager built on QEMU.
Documentation
use std::path::{Path, PathBuf};

use anyhow::{Context, Result, anyhow};

const MANAGED_INCLUDE_LINE: &str = "Include ~/.hardpass/ssh/config";
const LEGACY_INCLUDE_LINE: &str = "Include ~/.ssh/config.d/hardpass.conf";
const LEGACY_MANAGED_BEGIN: &str = "# >>> hardpass managed ssh include v1 >>>";
const LEGACY_MANAGED_END: &str = "# <<< hardpass managed ssh include v1 <<<";
const MANAGED_HEADER: &str = "# managed by hardpass; edits will be overwritten\n";

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SshAliasEntry {
    pub alias: String,
    pub host: String,
    pub port: u16,
    pub user: String,
    pub identity_file: PathBuf,
}

#[derive(Debug, Clone)]
pub struct SshConfigManager {
    home: PathBuf,
}

impl SshConfigManager {
    pub fn new(home: PathBuf) -> Self {
        Self { home }
    }

    pub fn from_home_dir() -> Result<Self> {
        let home = dirs::home_dir().ok_or_else(|| anyhow!("unable to determine home directory"))?;
        Ok(Self::new(home))
    }

    pub fn main_config_path(&self) -> PathBuf {
        self.ssh_dir().join("config")
    }

    pub fn managed_include_path(&self) -> PathBuf {
        self.managed_include_dir().join("config")
    }

    pub async fn install(&self) -> Result<()> {
        ensure_dir_mode(&self.ssh_dir(), 0o700).await?;
        ensure_dir_mode(&self.hardpass_dir(), 0o700).await?;
        ensure_dir_mode(&self.managed_include_dir(), 0o700).await?;
        let existing = match tokio::fs::read_to_string(self.main_config_path()).await {
            Ok(content) => content,
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(),
            Err(err) => return Err(err.into()),
        };
        let updated = install_managed_include(&existing);
        write_file_atomic(&self.main_config_path(), updated.as_bytes(), 0o600).await
    }

    pub async fn sync(&self, entries: &[SshAliasEntry]) -> Result<()> {
        ensure_dir_mode(&self.ssh_dir(), 0o700).await?;
        ensure_dir_mode(&self.hardpass_dir(), 0o700).await?;
        ensure_dir_mode(&self.managed_include_dir(), 0o700).await?;
        let rendered = render_managed_include(entries);
        write_file_atomic(&self.managed_include_path(), rendered.as_bytes(), 0o600).await
    }

    fn ssh_dir(&self) -> PathBuf {
        self.home.join(".ssh")
    }

    fn hardpass_dir(&self) -> PathBuf {
        self.home.join(".hardpass")
    }

    fn managed_include_dir(&self) -> PathBuf {
        self.hardpass_dir().join("ssh")
    }
}

fn install_managed_include(content: &str) -> String {
    let (without_integration, _) = remove_hardpass_integration(content);
    let insertion =
        first_host_or_match_offset(&without_integration).unwrap_or(without_integration.len());
    let before = without_integration[..insertion].trim_end_matches('\n');
    let after = without_integration[insertion..].trim_start_matches('\n');

    let mut updated = String::new();
    if before.is_empty() {
        updated.push_str(MANAGED_INCLUDE_LINE);
        updated.push('\n');
    } else {
        updated.push_str(before);
        updated.push_str("\n\n");
        updated.push_str(MANAGED_INCLUDE_LINE);
        updated.push('\n');
    }
    if !after.is_empty() {
        updated.push('\n');
        updated.push_str(after);
    }
    updated
}

fn remove_hardpass_integration(content: &str) -> (String, bool) {
    let mut updated = String::new();
    let mut found = false;
    let mut skipping_legacy_block = false;

    for line in content.split_inclusive('\n') {
        let trimmed = line.trim();
        if skipping_legacy_block {
            found = true;
            if trimmed == LEGACY_MANAGED_END {
                skipping_legacy_block = false;
            }
            continue;
        }
        if trimmed == LEGACY_MANAGED_BEGIN {
            found = true;
            skipping_legacy_block = true;
            continue;
        }
        if trimmed == MANAGED_INCLUDE_LINE || trimmed == LEGACY_INCLUDE_LINE {
            found = true;
            continue;
        }
        updated.push_str(line);
    }

    (updated, found)
}

fn first_host_or_match_offset(content: &str) -> Option<usize> {
    let mut offset = 0;
    for line in content.split_inclusive('\n') {
        if is_scoped_ssh_directive(line) {
            return Some(offset);
        }
        offset += line.len();
    }
    None
}

fn is_scoped_ssh_directive(line: &str) -> bool {
    let trimmed = line.trim_start();
    if trimmed.is_empty() || trimmed.starts_with('#') {
        return false;
    }
    let Some(keyword) = trimmed.split_whitespace().next() else {
        return false;
    };
    keyword.eq_ignore_ascii_case("host") || keyword.eq_ignore_ascii_case("match")
}

fn render_managed_include(entries: &[SshAliasEntry]) -> String {
    let mut entries = entries.to_vec();
    entries.sort_by(|left, right| left.alias.cmp(&right.alias));

    let mut output = String::from(MANAGED_HEADER);
    if entries.is_empty() {
        return output;
    }

    output.push('\n');
    for (index, entry) in entries.iter().enumerate() {
        if index > 0 {
            output.push('\n');
        }
        output.push_str(&format!("Host {}\n", entry.alias));
        output.push_str(&format!("    HostName {}\n", entry.host));
        output.push_str(&format!("    Port {}\n", entry.port));
        output.push_str(&format!("    User {}\n", entry.user));
        output.push_str(&format!(
            "    IdentityFile {}\n",
            ssh_config_quote_path(&entry.identity_file)
        ));
        output.push_str("    IdentitiesOnly yes\n");
        output.push_str("    StrictHostKeyChecking no\n");
        output.push_str("    UserKnownHostsFile /dev/null\n");
        output.push_str("    LogLevel ERROR\n");
    }
    output
}

fn ssh_config_quote_path(path: &Path) -> String {
    let text = path
        .display()
        .to_string()
        .replace('\\', "\\\\")
        .replace('"', "\\\"");
    format!("\"{text}\"")
}

async fn ensure_dir_mode(path: &Path, mode: u32) -> Result<()> {
    let path = path.to_path_buf();
    tokio::task::spawn_blocking(move || {
        use std::os::unix::fs::PermissionsExt;

        std::fs::create_dir_all(&path).with_context(|| format!("create dir {}", path.display()))?;
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode))
            .with_context(|| format!("chmod {:o} {}", mode, path.display()))
    })
    .await?
}

async fn write_file_atomic(path: &Path, payload: &[u8], mode: u32) -> Result<()> {
    let path = path.to_path_buf();
    let payload = payload.to_vec();
    tokio::task::spawn_blocking(move || {
        use std::io::Write;
        use std::os::unix::fs::PermissionsExt;

        let parent = path
            .parent()
            .ok_or_else(|| anyhow!("missing parent for {}", path.display()))?;
        std::fs::create_dir_all(parent)
            .with_context(|| format!("create dir {}", parent.display()))?;
        let tmp = path.with_extension(format!("tmp-{}", std::process::id()));
        let mut file =
            std::fs::File::create(&tmp).with_context(|| format!("create {}", tmp.display()))?;
        file.write_all(&payload)
            .with_context(|| format!("write {}", tmp.display()))?;
        file.sync_all()
            .with_context(|| format!("sync {}", tmp.display()))?;
        std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(mode))
            .with_context(|| format!("chmod {:o} {}", mode, tmp.display()))?;
        std::fs::rename(&tmp, &path)
            .with_context(|| format!("rename {} -> {}", tmp.display(), path.display()))?;
        Ok(())
    })
    .await?
}

#[cfg(test)]
mod tests {
    use std::os::unix::fs::PermissionsExt;
    use std::path::PathBuf;

    use tempfile::tempdir;

    use super::{
        MANAGED_INCLUDE_LINE, SshAliasEntry, SshConfigManager, install_managed_include,
        render_managed_include,
    };

    #[test]
    fn install_managed_include_inserts_into_empty_config() {
        let content = install_managed_include("");
        assert_eq!(content, format!("{MANAGED_INCLUDE_LINE}\n"));
    }

    #[test]
    fn install_managed_include_replaces_existing_block_idempotently() {
        let initial = "# comment\n\n# >>> hardpass managed ssh include v1 >>>\nInclude ~/.ssh/old\n# <<< hardpass managed ssh include v1 <<<\n";
        let content = install_managed_include(initial);
        assert_eq!(content.matches(MANAGED_INCLUDE_LINE).count(), 1);
        assert_eq!(install_managed_include(&content), content);
    }

    #[test]
    fn install_managed_include_places_include_before_first_host_block() {
        let initial = "# Added by OrbStack\nInclude ~/.orbstack/ssh/config\nInclude ~/.ssh/feathervm/config\n\nHost *.example.com\n  User ubuntu\n";
        let content = install_managed_include(initial);
        let include = content
            .find(MANAGED_INCLUDE_LINE)
            .expect("hardpass include");
        let host = content.find("Host *.example.com").expect("host block");
        assert!(
            include < host,
            "hardpass include must be before first Host block"
        );
        assert!(content.contains("Include ~/.orbstack/ssh/config"));
        assert!(content.contains("Include ~/.ssh/feathervm/config"));
    }

    #[test]
    fn install_managed_include_relocates_existing_block_out_of_host_scope() {
        let initial = "Host *.example.com\n  User ubuntu\n\n# >>> hardpass managed ssh include v1 >>>\nInclude ~/.ssh/old\n# <<< hardpass managed ssh include v1 <<<\n";
        let content = install_managed_include(initial);
        let include = content
            .find(MANAGED_INCLUDE_LINE)
            .expect("hardpass include");
        let host = content.find("Host *.example.com").expect("host block");
        assert!(
            include < host,
            "hardpass include must be before first Host block"
        );
        assert_eq!(content.matches(MANAGED_INCLUDE_LINE).count(), 1);
    }

    #[test]
    fn install_managed_include_relocates_existing_plain_include() {
        let initial = "Host *.example.com\n  User ubuntu\n\nInclude ~/.hardpass/ssh/config\n";
        let content = install_managed_include(initial);
        let include = content
            .find(MANAGED_INCLUDE_LINE)
            .expect("hardpass include");
        let host = content.find("Host *.example.com").expect("host block");
        assert!(include < host);
        assert_eq!(content.matches(MANAGED_INCLUDE_LINE).count(), 1);
    }

    #[test]
    fn render_managed_include_sorts_aliases_and_quotes_identity() {
        let rendered = render_managed_include(&[
            SshAliasEntry {
                alias: "beta".into(),
                host: "127.0.0.1".into(),
                port: 40222,
                user: "ubuntu".into(),
                identity_file: PathBuf::from("/tmp/path with spaces/id_ed25519"),
            },
            SshAliasEntry {
                alias: "alpha".into(),
                host: "127.0.0.1".into(),
                port: 40221,
                user: "ubuntu".into(),
                identity_file: PathBuf::from("/tmp/id_ed25519"),
            },
        ]);
        let alpha = rendered.find("Host alpha").expect("alpha host");
        let beta = rendered.find("Host beta").expect("beta host");
        assert!(alpha < beta);
        assert!(rendered.contains("IdentityFile \"/tmp/path with spaces/id_ed25519\""));
    }

    #[tokio::test]
    async fn install_and_sync_write_expected_files_with_permissions() {
        let dir = tempdir().expect("tempdir");
        let manager = SshConfigManager::new(dir.path().to_path_buf());
        manager.install().await.expect("install");
        manager
            .sync(&[SshAliasEntry {
                alias: "neuromancer".into(),
                host: "127.0.0.1".into(),
                port: 40222,
                user: "ubuntu".into(),
                identity_file: PathBuf::from("/tmp/id_ed25519"),
            }])
            .await
            .expect("sync");

        let config = tokio::fs::read_to_string(manager.main_config_path())
            .await
            .expect("read config");
        let include = tokio::fs::read_to_string(manager.managed_include_path())
            .await
            .expect("read include");
        assert!(config.contains(MANAGED_INCLUDE_LINE));
        assert!(include.contains("Host neuromancer"));

        let config_mode = std::fs::metadata(manager.main_config_path())
            .expect("config metadata")
            .permissions()
            .mode()
            & 0o777;
        let include_mode = std::fs::metadata(manager.managed_include_path())
            .expect("include metadata")
            .permissions()
            .mode()
            & 0o777;
        assert_eq!(config_mode, 0o600);
        assert_eq!(include_mode, 0o600);
    }
}