utils.rs
1 use anyhow::{Context, Result, bail}; 2 3 /// SSH connection information 4 #[derive(Debug, PartialEq)] 5 pub struct SshConnection { 6 pub hostname: String, 7 pub port: u16, 8 } 9 10 /// Parses a destination string into its components. 11 /// Supports both "user@host:/path" and "host:/path" formats. 12 /// When username is omitted, the current system username is used. 13 /// Resolves hostnames and ports using SSH config if available. 14 pub fn parse_destination(dest: &str) -> Result<(String, SshConnection, String)> { 15 let (user_host_part, path_part) = dest 16 .split_once(':') 17 .context("Invalid destination format. Expected 'host:/path' or 'user@host:/path'")?; 18 19 let (user, host) = if user_host_part.contains('@') { 20 let parts: Vec<&str> = user_host_part.splitn(2, '@').collect(); 21 (parts[0].to_string(), parts[1].to_string()) 22 } else { 23 // Use current username when not specified 24 let current_user = std::env::var("USER") 25 .or_else(|_| std::env::var("LOGNAME")) 26 .unwrap_or_else(|_| "unknown".to_string()); 27 (current_user, user_host_part.to_string()) 28 }; 29 30 // Resolve host and port using SSH config 31 let connection = resolve_ssh_connection(&host)?; 32 33 Ok((user, connection, path_part.to_string())) 34 } 35 36 /// Resolves hostname and port from SSH config 37 fn resolve_ssh_connection(host: &str) -> Result<SshConnection> { 38 // Try to use ssh_config crate if available, otherwise use ssh command 39 if let Ok(connection) = resolve_connection_with_ssh_config(host) { 40 return Ok(connection); 41 } 42 43 // Fallback: try to resolve using ssh -G command 44 if let Ok(connection) = resolve_connection_with_ssh_command(host) { 45 return Ok(connection); 46 } 47 48 // Final fallback: use host as-is with default port 49 Ok(SshConnection { 50 hostname: host.to_string(), 51 port: 22, 52 }) 53 } 54 55 /// Resolve hostname and port using ssh -G command 56 fn resolve_connection_with_ssh_command(host: &str) -> Result<SshConnection> { 57 use std::process::Command; 58 59 let output = Command::new("ssh") 60 .arg("-G") 61 .arg(host) 62 .output() 63 .context("Failed to run ssh -G command")?; 64 65 if !output.status.success() { 66 bail!("ssh -G command failed for host: {}", host); 67 } 68 69 let stdout = String::from_utf8_lossy(&output.stdout); 70 let mut hostname = host.to_string(); 71 let mut port = 22; 72 73 for line in stdout.lines() { 74 if line.starts_with("hostname ") { 75 hostname = line[9..].trim().to_string(); 76 } else if line.starts_with("port ") { 77 if let Ok(p) = line[5..].trim().parse::<u16>() { 78 port = p; 79 } 80 } 81 } 82 83 Ok(SshConnection { hostname, port }) 84 } 85 86 /// Resolve hostname and port using ssh_config crate (if available) 87 fn resolve_connection_with_ssh_config(host: &str) -> Result<SshConnection> { 88 // This is a simplified version - in a real implementation, you'd use the ssh_config crate 89 // For now, we'll check common SSH config locations manually 90 let home_dir = std::env::var("HOME").unwrap_or_else(|_| "/home".to_string()); 91 let config_paths = [ 92 format!("{}/.ssh/config", home_dir), 93 "/etc/ssh/ssh_config".to_string(), 94 ]; 95 96 for config_path in &config_paths { 97 if let Ok(content) = std::fs::read_to_string(config_path) { 98 if let Some(connection) = parse_ssh_config_connection(&content, host) { 99 return Ok(connection); 100 } 101 } 102 } 103 104 bail!("No SSH config found for host: {}", host) 105 } 106 107 /// Simple SSH config parser for hostname and port 108 fn parse_ssh_config_connection(config: &str, host: &str) -> Option<SshConnection> { 109 let mut current_host = None; 110 let mut hostname = None; 111 let mut port = 22; 112 113 for line in config.lines() { 114 let line = line.trim(); 115 if line.is_empty() || line.starts_with('#') { 116 continue; 117 } 118 119 let parts: Vec<&str> = line.split_whitespace().collect(); 120 if parts.len() < 2 { 121 continue; 122 } 123 124 match parts[0].to_lowercase().as_str() { 125 "host" => { 126 current_host = Some(parts[1]); 127 hostname = None; // Reset for new host 128 port = 22; // Reset to default 129 } 130 "hostname" => { 131 if let Some(h) = current_host { 132 if h == host || (h == "*" && hostname.is_none()) { 133 hostname = Some(parts[1].to_string()); 134 } 135 } 136 } 137 "port" => { 138 if let Some(h) = current_host { 139 if h == host || (h == "*") { 140 if let Ok(p) = parts[1].parse::<u16>() { 141 port = p; 142 } 143 } 144 } 145 } 146 _ => {} 147 } 148 } 149 150 hostname.map(|h| SshConnection { hostname: h, port }) 151 }