ssh-portkey 0.1.3

Secure SSH credential manager with a fast ratatui-based TUI, fuzzy search, and encrypted vault
Documentation
use anyhow::{anyhow, Result};
use std::process::Command;

use crate::models::Server;

fn command_exists(command: &str) -> bool {
    Command::new("which")
        .arg(command)
        .output()
        .map(|output| output.status.success())
        .unwrap_or(false)
}

pub fn build_ssh_args(server: &Server) -> Vec<String> {
    let mut args = vec!["-tt".to_string()];

    if let Some(identity_file) = server
        .identity_file
        .as_deref()
        .filter(|path| !path.is_empty())
    {
        args.push("-i".to_string());
        args.push(identity_file.to_string());
    }

    if server.forward_agent {
        args.push("-A".to_string());
    }

    args.push("-p".to_string());
    args.push(server.port.to_string());
    args.push(format!("{}@{}", server.username, server.host));
    args
}

fn shell_quote(arg: &str) -> String {
    if arg
        .chars()
        .all(|c| !c.is_whitespace() && !matches!(c, '\'' | '"' | '\\' | '$' | '`'))
    {
        arg.to_string()
    } else {
        format!("'{}'", arg.replace('\'', "'\\''"))
    }
}

fn ssh_command_line(server: &Server) -> String {
    let args = build_ssh_args(server)
        .iter()
        .map(|arg| shell_quote(arg))
        .collect::<Vec<_>>()
        .join(" ");
    format!("ssh {args}")
}

pub fn manual_connection_help(server: &Server) -> String {
    format!(
        "Connect manually with:\n  {}\nPassword is stored in Portkey and will not be printed.",
        ssh_command_line(server)
    )
}

pub fn connect(server: &Server) -> Result<()> {
    println!(
        "Connecting to {}@{}:{}...",
        server.username, server.host, server.port
    );

    if !command_exists("ssh") {
        return Err(anyhow!("ssh is not installed or not in PATH"));
    }

    let ssh_args = build_ssh_args(server);
    let has_password = !server.password.is_empty();

    let status = if has_password {
        if !command_exists("sshpass") {
            eprintln!("❌ sshpass is not installed or not in PATH.");
            eprintln!();
            eprintln!("Install sshpass to use password authentication:");
            eprintln!("  macOS: brew install hudochenkov/sshpass/sshpass");
            eprintln!("  Ubuntu/Debian: sudo apt-get install sshpass");
            eprintln!("  CentOS/RHEL: sudo yum install sshpass");
            eprintln!("  Arch: sudo pacman -S sshpass");
            eprintln!();
            eprintln!("{}", manual_connection_help(server));
            return Err(anyhow!(
                "sshpass is required for stored password authentication"
            ));
        }

        Command::new("sshpass")
            .env("SSHPASS", &server.password)
            .env(
                "TERM",
                std::env::var("TERM").unwrap_or_else(|_| "xterm-256color".to_string()),
            )
            .arg("-e")
            .arg("ssh")
            .args(&ssh_args)
            .status()?
    } else {
        Command::new("ssh")
            .env(
                "TERM",
                std::env::var("TERM").unwrap_or_else(|_| "xterm-256color".to_string()),
            )
            .args(&ssh_args)
            .status()?
    };

    if status.success() {
        Ok(())
    } else {
        Err(anyhow!(
            "SSH connection failed. Possible causes: server unreachable, invalid credentials, SSH service not running, or port blocked by firewall"
        ))
    }
}