Skip to main content

portkey/
ssh.rs

1use anyhow::{anyhow, Result};
2use std::process::Command;
3
4use crate::models::Server;
5
6fn command_exists(command: &str) -> bool {
7    Command::new("which")
8        .arg(command)
9        .output()
10        .map(|output| output.status.success())
11        .unwrap_or(false)
12}
13
14pub fn build_ssh_args(server: &Server) -> Vec<String> {
15    let mut args = vec!["-tt".to_string()];
16
17    if let Some(identity_file) = server
18        .identity_file
19        .as_deref()
20        .filter(|path| !path.is_empty())
21    {
22        args.push("-i".to_string());
23        args.push(identity_file.to_string());
24    }
25
26    if server.forward_agent {
27        args.push("-A".to_string());
28    }
29
30    args.push("-p".to_string());
31    args.push(server.port.to_string());
32    args.push(format!("{}@{}", server.username, server.host));
33    args
34}
35
36fn shell_quote(arg: &str) -> String {
37    if arg
38        .chars()
39        .all(|c| !c.is_whitespace() && !matches!(c, '\'' | '"' | '\\' | '$' | '`'))
40    {
41        arg.to_string()
42    } else {
43        format!("'{}'", arg.replace('\'', "'\\''"))
44    }
45}
46
47fn ssh_command_line(server: &Server) -> String {
48    let args = build_ssh_args(server)
49        .iter()
50        .map(|arg| shell_quote(arg))
51        .collect::<Vec<_>>()
52        .join(" ");
53    format!("ssh {args}")
54}
55
56pub fn manual_connection_help(server: &Server) -> String {
57    format!(
58        "Connect manually with:\n  {}\nPassword is stored in Portkey and will not be printed.",
59        ssh_command_line(server)
60    )
61}
62
63pub fn connect(server: &Server) -> Result<()> {
64    println!(
65        "Connecting to {}@{}:{}...",
66        server.username, server.host, server.port
67    );
68
69    if !command_exists("ssh") {
70        return Err(anyhow!("ssh is not installed or not in PATH"));
71    }
72
73    let ssh_args = build_ssh_args(server);
74    let has_password = !server.password.is_empty();
75
76    let status = if has_password {
77        if !command_exists("sshpass") {
78            eprintln!("❌ sshpass is not installed or not in PATH.");
79            eprintln!();
80            eprintln!("Install sshpass to use password authentication:");
81            eprintln!("  macOS: brew install hudochenkov/sshpass/sshpass");
82            eprintln!("  Ubuntu/Debian: sudo apt-get install sshpass");
83            eprintln!("  CentOS/RHEL: sudo yum install sshpass");
84            eprintln!("  Arch: sudo pacman -S sshpass");
85            eprintln!();
86            eprintln!("{}", manual_connection_help(server));
87            return Err(anyhow!(
88                "sshpass is required for stored password authentication"
89            ));
90        }
91
92        Command::new("sshpass")
93            .env("SSHPASS", &server.password)
94            .env(
95                "TERM",
96                std::env::var("TERM").unwrap_or_else(|_| "xterm-256color".to_string()),
97            )
98            .arg("-e")
99            .arg("ssh")
100            .args(&ssh_args)
101            .status()?
102    } else {
103        Command::new("ssh")
104            .env(
105                "TERM",
106                std::env::var("TERM").unwrap_or_else(|_| "xterm-256color".to_string()),
107            )
108            .args(&ssh_args)
109            .status()?
110    };
111
112    if status.success() {
113        Ok(())
114    } else {
115        Err(anyhow!(
116            "SSH connection failed. Possible causes: server unreachable, invalid credentials, SSH service not running, or port blocked by firewall"
117        ))
118    }
119}