|
| 1 | +use serde::{Deserialize, Serialize}; |
| 2 | +use std::net::TcpStream; |
| 3 | +use std::process::Command; |
| 4 | +use std::time::Duration; |
| 5 | + |
| 6 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 7 | +pub struct PortScanResult { |
| 8 | + pub port: u16, |
| 9 | + pub is_open: bool, |
| 10 | + pub service: Option<String>, |
| 11 | +} |
| 12 | + |
| 13 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 14 | +pub struct ScanOptions { |
| 15 | + pub host: String, |
| 16 | + pub start_port: u16, |
| 17 | + pub end_port: u16, |
| 18 | + pub use_nmap: bool, |
| 19 | + pub timeout_ms: u64, |
| 20 | +} |
| 21 | + |
| 22 | +/// Scan a single port using native TCP connection |
| 23 | +pub fn scan_port_native(host: &str, port: u16, timeout_ms: u64) -> PortScanResult { |
| 24 | + let addr = format!("{}:{}", host, port); |
| 25 | + let timeout = Duration::from_millis(timeout_ms); |
| 26 | + |
| 27 | + let is_open = TcpStream::connect_timeout( |
| 28 | + &addr.parse().unwrap_or_else(|_| { |
| 29 | + format!("127.0.0.1:{}", port).parse().unwrap() |
| 30 | + }), |
| 31 | + timeout, |
| 32 | + ) |
| 33 | + .is_ok(); |
| 34 | + |
| 35 | + PortScanResult { |
| 36 | + port, |
| 37 | + is_open, |
| 38 | + service: if is_open { get_service_name(port) } else { None }, |
| 39 | + } |
| 40 | +} |
| 41 | + |
| 42 | +/// Get common service name for a port |
| 43 | +fn get_service_name(port: u16) -> Option<String> { |
| 44 | + let service = match port { |
| 45 | + 20 => "FTP Data", |
| 46 | + 21 => "FTP", |
| 47 | + 22 => "SSH", |
| 48 | + 23 => "Telnet", |
| 49 | + 25 => "SMTP", |
| 50 | + 53 => "DNS", |
| 51 | + 80 => "HTTP", |
| 52 | + 110 => "POP3", |
| 53 | + 143 => "IMAP", |
| 54 | + 443 => "HTTPS", |
| 55 | + 445 => "SMB", |
| 56 | + 3306 => "MySQL", |
| 57 | + 3389 => "RDP", |
| 58 | + 5432 => "PostgreSQL", |
| 59 | + 5900 => "VNC", |
| 60 | + 6379 => "Redis", |
| 61 | + 8080 => "HTTP-Alt", |
| 62 | + 8443 => "HTTPS-Alt", |
| 63 | + 27017 => "MongoDB", |
| 64 | + _ => return None, |
| 65 | + }; |
| 66 | + Some(service.to_string()) |
| 67 | +} |
| 68 | + |
| 69 | +/// Scan ports using native TCP method |
| 70 | +pub fn scan_ports_native(options: &ScanOptions) -> Vec<PortScanResult> { |
| 71 | + let mut results = Vec::new(); |
| 72 | + |
| 73 | + for port in options.start_port..=options.end_port { |
| 74 | + let result = scan_port_native(&options.host, port, options.timeout_ms); |
| 75 | + results.push(result); |
| 76 | + } |
| 77 | + |
| 78 | + results |
| 79 | +} |
| 80 | + |
| 81 | +/// Scan ports using nmap command |
| 82 | +pub fn scan_ports_nmap(options: &ScanOptions) -> Result<Vec<PortScanResult>, String> { |
| 83 | + let port_range = if options.start_port == options.end_port { |
| 84 | + format!("{}", options.start_port) |
| 85 | + } else { |
| 86 | + format!("{}-{}", options.start_port, options.end_port) |
| 87 | + }; |
| 88 | + |
| 89 | + let output = Command::new("nmap") |
| 90 | + .arg("-p") |
| 91 | + .arg(&port_range) |
| 92 | + .arg(&options.host) |
| 93 | + .output(); |
| 94 | + |
| 95 | + match output { |
| 96 | + Ok(output) => { |
| 97 | + let stdout = String::from_utf8_lossy(&output.stdout); |
| 98 | + parse_nmap_output(&stdout, options.start_port, options.end_port) |
| 99 | + } |
| 100 | + Err(e) => Err(format!("Failed to execute nmap: {}. Make sure nmap is installed.", e)), |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +/// Parse nmap output |
| 105 | +fn parse_nmap_output(output: &str, start_port: u16, end_port: u16) -> Result<Vec<PortScanResult>, String> { |
| 106 | + let mut results = Vec::new(); |
| 107 | + |
| 108 | + for line in output.lines() { |
| 109 | + if line.contains("/tcp") || line.contains("/udp") { |
| 110 | + let parts: Vec<&str> = line.split_whitespace().collect(); |
| 111 | + if parts.len() >= 2 { |
| 112 | + if let Some(port_str) = parts[0].split('/').next() { |
| 113 | + if let Ok(port) = port_str.parse::<u16>() { |
| 114 | + let is_open = parts[1].contains("open"); |
| 115 | + let service = if parts.len() >= 3 { |
| 116 | + Some(parts[2].to_string()) |
| 117 | + } else { |
| 118 | + get_service_name(port) |
| 119 | + }; |
| 120 | + |
| 121 | + results.push(PortScanResult { |
| 122 | + port, |
| 123 | + is_open, |
| 124 | + service, |
| 125 | + }); |
| 126 | + } |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + // If nmap didn't return results for all ports, fill in the gaps |
| 133 | + if results.is_empty() { |
| 134 | + for port in start_port..=end_port { |
| 135 | + results.push(PortScanResult { |
| 136 | + port, |
| 137 | + is_open: false, |
| 138 | + service: None, |
| 139 | + }); |
| 140 | + } |
| 141 | + } |
| 142 | + |
| 143 | + Ok(results) |
| 144 | +} |
| 145 | + |
| 146 | +/// Main scan function that chooses between native and nmap |
| 147 | +pub fn scan_ports(options: ScanOptions) -> Result<Vec<PortScanResult>, String> { |
| 148 | + if options.use_nmap { |
| 149 | + scan_ports_nmap(&options) |
| 150 | + } else { |
| 151 | + Ok(scan_ports_native(&options)) |
| 152 | + } |
| 153 | +} |
0 commit comments