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}