use netstat2::{get_sockets_info, AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo};
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
use std::process::Output;
use std::time::{Duration, Instant};
use tokio::process::Command;
use crate::logging::{log_info, log_timed, LogLevel};
pub async fn run_ports(args: &[String], debug: bool) -> Result<(), String> {
let mut port_filter: Option<String> = None;
let mut kill: bool = false;
let mut nginx_search: bool = false;
let mut i: usize = 0;
while i < args.len() {
match args[i].as_str() {
"-p" => {
if let Some(p) = args.get(i + 1) {
port_filter = Some(p.clone());
i += 2;
} else {
return Err("-p requires a port value".to_string());
}
}
"--kill" => {
kill = true;
i += 1;
}
"-n" => {
nginx_search = true;
i += 1;
}
_ => {
i += 1;
}
}
}
if debug {
println!("\x1b[94mDebug mode enabled\x1b[0m");
println!("[DEBUG] Args: {:?}", args);
}
let _ = log_info("ports", "Executing ports command", port_filter.as_deref()).await;
let start: Instant = Instant::now();
let command_output: String = execute_ports_command_netstat2(port_filter.clone(), debug, kill).await;
let elapsed: Duration = start.elapsed();
let _ = log_timed(
LogLevel::Success,
"ports",
"Ports command completed",
elapsed.as_millis() as u64,
)
.await;
if debug {
println!("[DEBUG] execute_ports_command took: {:.2?}", elapsed);
}
if command_output.trim().is_empty() {
if let Some(port) = port_filter.clone() {
println!("\x1b[94mNo active processes found on port: {}\x1b[0m", port);
}
} else {
display_output(command_output);
}
if nginx_search {
if let Some(port) = port_filter {
println!("\nSearching NGINX configurations for port: {}\n", port);
search_nginx_configs(&port).await;
} else {
eprintln!("\x1b[91mError: -n flag requires a port to be specified with -p.\x1b[0m");
}
}
Ok(())
}
async fn execute_ports_command_netstat2(
port_filter: Option<String>,
debug: bool,
kill: bool,
) -> String {
let start: Instant = Instant::now();
if let Some(ref port) = port_filter {
return get_port_info_with_netstat(port, debug, kill).await;
}
let af_flags: AddressFamilyFlags = AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6;
let proto_flags: ProtocolFlags = ProtocolFlags::TCP;
let sockets: Vec<netstat2::SocketInfo> = match get_sockets_info(af_flags, proto_flags) {
Ok(s) => s,
Err(e) => {
return format!("Failed to get sockets info: {}", e);
}
};
if debug {
println!("[DEBUG] Fetched {} TCP sockets", sockets.len());
}
let mut table_output: String = String::new();
let mut port_map: BTreeMap<u16, Vec<(&netstat2::SocketInfo, &netstat2::TcpSocketInfo)>> =
BTreeMap::new();
for socket in &sockets {
if let ProtocolSocketInfo::Tcp(ref tcp_info) = socket.protocol_socket_info {
port_map
.entry(tcp_info.local_port)
.or_default()
.push((socket, tcp_info));
}
}
for (port, entries) in &port_map {
table_output.push_str(&format!("Port: {}\n", port));
table_output.push_str(&format!(
"{:<10} {:<20} {:<20} {:<10} {:<10}\n",
"PID", "LocalAddr", "RemoteAddr", "State", "Process"
));
table_output.push_str(&format!("{:-<80}\n", ""));
for (socket, tcp_info) in entries {
let pids: String = if !socket.associated_pids.is_empty() {
socket
.associated_pids
.iter()
.map(|pid| pid.to_string())
.collect::<Vec<_>>()
.join(",")
} else {
"-".to_string()
};
let process_names: String = "-".to_string();
table_output.push_str(&format!(
"{:<10} {:<20} {:<20} {:<10} {:<10}\n",
pids,
tcp_info.local_addr,
tcp_info.remote_addr,
format!("{:?}", tcp_info.state),
process_names
));
if kill {
for pid in &socket.associated_pids {
let killed: bool = kill_process_with_debug(&pid.to_string(), debug).await;
if killed {
println!(
"\x1b[92mSuccessfully killed process with PID: {}\x1b[0m",
pid
);
table_output.push_str(&format!(
"\x1b[91mKilled process with PID: {}\x1b[0m\n",
pid
));
} else {
eprintln!("\x1b[91mFailed to kill process with PID: {}\x1b[0m", pid);
}
}
}
}
table_output.push_str(&format!("{:-<80}\n\n", ""));
}
if debug {
println!(
"[DEBUG] execute_ports_command_netstat2 took: {:.2?}",
start.elapsed()
);
}
table_output
}
async fn get_port_info_with_netstat(port: &str, debug: bool, kill: bool) -> String {
if debug {
println!("[DEBUG] Using netstat to get PIDs for port: {}", port);
}
let netstat_cmd = format!("sudo netstat -tulpen | grep :{}", port);
let output = Command::new("sh")
.arg("-c")
.arg(&netstat_cmd)
.output()
.await;
let output = match output {
Ok(o) => o,
Err(e) => {
return format!("Failed to execute netstat: {}", e);
}
};
if !output.status.success() && output.stdout.is_empty() {
return format!("No processes found on port: {}", port);
}
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.trim().is_empty() {
return format!("No processes found on port: {}", port);
}
let mut table_output = String::new();
table_output.push_str(&format!("Port: {}\n", port));
table_output.push_str(&format!(
"{:<10} {:<20} {:<20} {:<10} {:<10} {:<10}\n",
"PID", "Proto", "LocalAddr", "ForeignAddr", "State", "Program"
));
table_output.push_str(&format!("{:-<90}\n", ""));
let mut pids_to_kill: Vec<String> = Vec::new();
for line in stdout.lines() {
if line.trim().is_empty() {
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 7 {
continue;
}
let pid = if parts.len() >= 7 {
let pid_program = parts[parts.len() - 1];
if let Some(slash_pos) = pid_program.find('/') {
&pid_program[..slash_pos]
} else {
pid_program
}
} else {
"-"
};
let proto = parts.get(0).unwrap_or(&"-");
let local_addr = parts.get(3).unwrap_or(&"-");
let foreign_addr = parts.get(4).unwrap_or(&"-");
let state = parts.get(parts.len() - 2).unwrap_or(&"-");
let program = if parts.len() >= 7 {
let pid_program = parts[parts.len() - 1];
if let Some(slash_pos) = pid_program.find('/') {
&pid_program[slash_pos + 1..]
} else {
"-"
}
} else {
"-"
};
table_output.push_str(&format!(
"{:<10} {:<20} {:<20} {:<20} {:<10} {:<10}\n",
pid, proto, local_addr, foreign_addr, state, program
));
if kill && pid != "-" {
pids_to_kill.push(pid.to_string());
}
}
table_output.push_str(&format!("{:-<90}\n", ""));
if kill {
for pid in pids_to_kill {
let killed = kill_process_with_debug(&pid, debug).await;
if killed {
println!("\x1b[92mSuccessfully killed process with PID: {}\x1b[0m", pid);
table_output.push_str(&format!("\x1b[91mKilled process with PID: {}\x1b[0m\n", pid));
} else {
eprintln!("\x1b[91mFailed to kill process with PID: {}\x1b[0m", pid);
}
}
}
table_output
}
async fn kill_process_with_debug(pid: &str, debug: bool) -> bool {
if debug {
println!("[DEBUG] Attempting to kill PID: {}", pid);
}
let start: Instant = Instant::now();
let kill_output: Output = Command::new("sh")
.arg("-c")
.arg(format!("sudo kill -9 {}", pid))
.output()
.await
.expect("Failed to execute kill command");
let elapsed = start.elapsed();
if debug {
println!(
"[DEBUG] kill_process output: status={:?}, stdout='{}', stderr='{}', took: {:.2?}",
kill_output.status,
String::from_utf8_lossy(&kill_output.stdout),
String::from_utf8_lossy(&kill_output.stderr),
elapsed
);
}
kill_output.status.success()
}
fn display_output(output: String) {
println!("{}", output);
}
async fn search_nginx_configs(port: &str) {
let nginx_sites_available_path = PathBuf::from("/etc/nginx/sites-available");
if !nginx_sites_available_path.exists() {
println!("\x1b[93mWarning: /etc/nginx/sites-available/ not found. Skipping NGINX config search.\x1b[0m");
return;
}
let mut found_configs = false;
match fs::read_dir(&nginx_sites_available_path) {
Ok(entries) => {
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_file() {
let config_content = match fs::read_to_string(&path) {
Ok(content) => content,
Err(_) => continue,
};
if config_content.contains(&format!("proxy_pass http://127.0.0.1:{}", port))
|| config_content.contains(&format!("listen {}", port))
{
println!("\x1b[92mFound port {} in NGINX config: {}\x1b[0m", port, path.display());
found_configs = true;
}
}
}
}
}
Err(e) => {
eprintln!("\x1b[91mError reading NGINX sites-available directory: {}\x1b[0m", e);
return;
}
}
if !found_configs {
println!("\x1b[94mNo NGINX configurations found for port {}.\x1b[0m", port);
}
}