bhcli

A TUI for chatting on LE PHP Chats
git clone https://git.dasho.dev/bhcli.git
Log | Files | Refs | README

network.rs (15078B)


      1 use crate::chatops::{ChatCommand, ChatOpError, ChatOpResult, CommandContext};
      2 use std::process::Command;
      3 
      4 /// Ping command
      5 pub struct PingCommand;
      6 
      7 impl ChatCommand for PingCommand {
      8    fn name(&self) -> &'static str {
      9        "ping"
     10    }
     11    fn description(&self) -> &'static str {
     12        "Ping a host"
     13    }
     14    fn usage(&self) -> &'static str {
     15        "/ping <host>"
     16    }
     17 
     18    fn execute(
     19        &self,
     20        args: Vec<String>,
     21        _context: &CommandContext,
     22    ) -> Result<ChatOpResult, ChatOpError> {
     23        if args.is_empty() {
     24            return Err(ChatOpError::MissingArguments(
     25                "Please specify a host to ping".to_string(),
     26            ));
     27        }
     28 
     29        let host = &args[0];
     30 
     31        match Command::new("ping")
     32            .args(&["-c", "3", host]) // 3 packets on Unix
     33            .output()
     34        {
     35            Ok(output) => {
     36                if output.status.success() {
     37                    let result = String::from_utf8_lossy(&output.stdout);
     38                    let lines: Vec<String> = result
     39                        .lines()
     40                        .filter(|line| {
     41                            line.contains("time=")
     42                                || line.contains("packet loss")
     43                                || line.contains("min/avg/max")
     44                        })
     45                        .take(5)
     46                        .map(|line| format!("🏓 {}", line.trim()))
     47                        .collect();
     48 
     49                    if lines.is_empty() {
     50                        Ok(ChatOpResult::Message(format!(
     51                            "🏓 Ping to {} completed",
     52                            host
     53                        )))
     54                    } else {
     55                        Ok(ChatOpResult::Block(lines))
     56                    }
     57                } else {
     58                    Ok(ChatOpResult::Message(format!("🏓 Ping to {} failed", host)))
     59                }
     60            }
     61            Err(_) => Ok(ChatOpResult::Message(format!(
     62                "🏓 Ping command not available"
     63            ))),
     64        }
     65    }
     66 }
     67 
     68 /// Traceroute command
     69 pub struct TraceCommand;
     70 
     71 impl ChatCommand for TraceCommand {
     72    fn name(&self) -> &'static str {
     73        "trace"
     74    }
     75    fn description(&self) -> &'static str {
     76        "Trace route to host"
     77    }
     78    fn usage(&self) -> &'static str {
     79        "/trace <host>"
     80    }
     81    fn aliases(&self) -> Vec<&'static str> {
     82        vec!["traceroute"]
     83    }
     84 
     85    fn execute(
     86        &self,
     87        args: Vec<String>,
     88        _context: &CommandContext,
     89    ) -> Result<ChatOpResult, ChatOpError> {
     90        if args.is_empty() {
     91            return Err(ChatOpError::MissingArguments(
     92                "Please specify a host to trace".to_string(),
     93            ));
     94        }
     95 
     96        let host = &args[0];
     97 
     98        match Command::new("traceroute")
     99            .args(&["-m", "10", host]) // Max 10 hops
    100            .output()
    101        {
    102            Ok(output) => {
    103                if output.status.success() {
    104                    let result = String::from_utf8_lossy(&output.stdout);
    105                    let lines: Vec<String> = result
    106                        .lines()
    107                        .take(12) // Limit output
    108                        .map(|line| format!("📍 {}", line.trim()))
    109                        .collect();
    110 
    111                    if lines.is_empty() {
    112                        Ok(ChatOpResult::Message(format!(
    113                            "📍 Traceroute to {} completed",
    114                            host
    115                        )))
    116                    } else {
    117                        Ok(ChatOpResult::Block(lines))
    118                    }
    119                } else {
    120                    Ok(ChatOpResult::Message(format!(
    121                        "📍 Traceroute to {} failed",
    122                        host
    123                    )))
    124                }
    125            }
    126            Err(_) => {
    127                // Try with tracepath as alternative
    128                match Command::new("tracepath").arg(host).output() {
    129                    Ok(output) if output.status.success() => {
    130                        let result = String::from_utf8_lossy(&output.stdout);
    131                        let lines: Vec<String> = result
    132                            .lines()
    133                            .take(12)
    134                            .map(|line| format!("📍 {}", line.trim()))
    135                            .collect();
    136                        Ok(ChatOpResult::Block(lines))
    137                    }
    138                    _ => Ok(ChatOpResult::Message(
    139                        "📍 Traceroute command not available".to_string(),
    140                    )),
    141                }
    142            }
    143        }
    144    }
    145 }
    146 
    147 /// Port scan command
    148 pub struct PortScanCommand;
    149 
    150 impl ChatCommand for PortScanCommand {
    151    fn name(&self) -> &'static str {
    152        "portscan"
    153    }
    154    fn description(&self) -> &'static str {
    155        "Simple port scan (TCP connect)"
    156    }
    157    fn usage(&self) -> &'static str {
    158        "/portscan <host> [ports]"
    159    }
    160 
    161    fn execute(
    162        &self,
    163        args: Vec<String>,
    164        _context: &CommandContext,
    165    ) -> Result<ChatOpResult, ChatOpError> {
    166        if args.is_empty() {
    167            return Err(ChatOpError::MissingArguments(
    168                "Please specify a host to scan".to_string(),
    169            ));
    170        }
    171 
    172        let host = &args[0];
    173        let ports = args.get(1).unwrap_or(&"22,80,443".to_string()).clone();
    174 
    175        // Simple TCP connect scan using netcat if available
    176        match Command::new("nc")
    177            .args(&["-z", "-v", "-w", "2", host, &ports])
    178            .output()
    179        {
    180            Ok(output) => {
    181                let stdout = String::from_utf8_lossy(&output.stdout);
    182                let stderr = String::from_utf8_lossy(&output.stderr);
    183                let result = format!("{}{}", stdout, stderr);
    184 
    185                let lines: Vec<String> = result
    186                    .lines()
    187                    .filter(|line| !line.trim().is_empty())
    188                    .take(10)
    189                    .map(|line| format!("🔍 {}", line.trim()))
    190                    .collect();
    191 
    192                if lines.is_empty() {
    193                    Ok(ChatOpResult::Message(format!(
    194                        "🔍 Port scan of {} completed (no open ports found)",
    195                        host
    196                    )))
    197                } else {
    198                    Ok(ChatOpResult::Block(lines))
    199                }
    200            }
    201            Err(_) => {
    202                // Try with nmap if available
    203                match Command::new("nmap").args(&["-p", &ports, host]).output() {
    204                    Ok(output) if output.status.success() => {
    205                        let result = String::from_utf8_lossy(&output.stdout);
    206                        let lines: Vec<String> = result
    207                            .lines()
    208                            .filter(|line| {
    209                                line.contains("/tcp") || line.contains("Nmap scan report")
    210                            })
    211                            .take(10)
    212                            .map(|line| format!("🔍 {}", line.trim()))
    213                            .collect();
    214                        Ok(ChatOpResult::Block(lines))
    215                    }
    216                    _ => Ok(ChatOpResult::Message(
    217                        "🔍 Port scanning tools not available (nc/nmap)".to_string(),
    218                    )),
    219                }
    220            }
    221        }
    222    }
    223 }
    224 
    225 /// HTTP headers command
    226 pub struct HeadersCommand;
    227 
    228 impl ChatCommand for HeadersCommand {
    229    fn name(&self) -> &'static str {
    230        "headers"
    231    }
    232    fn description(&self) -> &'static str {
    233        "Show HTTP response headers"
    234    }
    235    fn usage(&self) -> &'static str {
    236        "/headers <url>"
    237    }
    238 
    239    fn execute(
    240        &self,
    241        args: Vec<String>,
    242        _context: &CommandContext,
    243    ) -> Result<ChatOpResult, ChatOpError> {
    244        if args.is_empty() {
    245            return Err(ChatOpError::MissingArguments(
    246                "Please specify a URL".to_string(),
    247            ));
    248        }
    249 
    250        let url = &args[0];
    251 
    252        match Command::new("curl").args(&["-I", "-s", url]).output() {
    253            Ok(output) => {
    254                if output.status.success() {
    255                    let result = String::from_utf8_lossy(&output.stdout);
    256                    let lines: Vec<String> = result
    257                        .lines()
    258                        .filter(|line| !line.trim().is_empty())
    259                        .take(15) // Limit headers
    260                        .map(|line| format!("📡 {}", line.trim()))
    261                        .collect();
    262 
    263                    if lines.is_empty() {
    264                        Ok(ChatOpResult::Message(format!(
    265                            "📡 No headers received from {}",
    266                            url
    267                        )))
    268                    } else {
    269                        Ok(ChatOpResult::Block(lines))
    270                    }
    271                } else {
    272                    Ok(ChatOpResult::Message(format!(
    273                        "📡 Failed to fetch headers from {}",
    274                        url
    275                    )))
    276                }
    277            }
    278            Err(_) => Ok(ChatOpResult::Message(
    279                "📡 curl command not available".to_string(),
    280            )),
    281        }
    282    }
    283 }
    284 
    285 /// Simple HTTP request command
    286 pub struct CurlCommand;
    287 
    288 impl ChatCommand for CurlCommand {
    289    fn name(&self) -> &'static str {
    290        "curl"
    291    }
    292    fn description(&self) -> &'static str {
    293        "Make HTTP request and show response"
    294    }
    295    fn usage(&self) -> &'static str {
    296        "/curl <url>"
    297    }
    298 
    299    fn execute(
    300        &self,
    301        args: Vec<String>,
    302        _context: &CommandContext,
    303    ) -> Result<ChatOpResult, ChatOpError> {
    304        if args.is_empty() {
    305            return Err(ChatOpError::MissingArguments(
    306                "Please specify a URL".to_string(),
    307            ));
    308        }
    309 
    310        let url = &args[0];
    311 
    312        match Command::new("curl")
    313            .args(&["-s", "-L", "--max-time", "10", url])
    314            .output()
    315        {
    316            Ok(output) => {
    317                if output.status.success() {
    318                    let result = String::from_utf8_lossy(&output.stdout);
    319                    let lines: Vec<String> = result
    320                        .lines()
    321                        .take(20) // Limit response body
    322                        .map(|line| line.to_string())
    323                        .collect();
    324 
    325                    if lines.is_empty() {
    326                        Ok(ChatOpResult::Message(format!(
    327                            "🌐 Empty response from {}",
    328                            url
    329                        )))
    330                    } else {
    331                        Ok(ChatOpResult::CodeBlock(
    332                            lines.join("\n"),
    333                            Some("text".to_string()),
    334                        ))
    335                    }
    336                } else {
    337                    Ok(ChatOpResult::Message(format!("🌐 Failed to fetch {}", url)))
    338                }
    339            }
    340            Err(_) => Ok(ChatOpResult::Message(
    341                "🌐 curl command not available".to_string(),
    342            )),
    343        }
    344    }
    345 }
    346 
    347 /// SSL certificate information
    348 pub struct SslCommand;
    349 
    350 impl ChatCommand for SslCommand {
    351    fn name(&self) -> &'static str {
    352        "ssl"
    353    }
    354    fn description(&self) -> &'static str {
    355        "Show SSL certificate information"
    356    }
    357    fn usage(&self) -> &'static str {
    358        "/ssl <domain>"
    359    }
    360    fn aliases(&self) -> Vec<&'static str> {
    361        vec!["cert"]
    362    }
    363 
    364    fn execute(
    365        &self,
    366        args: Vec<String>,
    367        _context: &CommandContext,
    368    ) -> Result<ChatOpResult, ChatOpError> {
    369        if args.is_empty() {
    370            return Err(ChatOpError::MissingArguments(
    371                "Please specify a domain".to_string(),
    372            ));
    373        }
    374 
    375        let domain = &args[0];
    376 
    377        match Command::new("openssl")
    378            .args(&["s_client", "-connect", &format!("{}:443", domain), "-servername", domain])
    379            .stdin(std::process::Stdio::null())
    380            .output()
    381        {
    382            Ok(output) => {
    383                let result = String::from_utf8_lossy(&output.stdout);
    384                let lines: Vec<String> = result
    385                    .lines()
    386                    .filter(|line| {
    387                        line.contains("subject=") ||
    388                        line.contains("issuer=") ||
    389                        line.contains("notBefore=") ||
    390                        line.contains("notAfter=") ||
    391                        line.contains("Verification:")
    392                    })
    393                    .take(8)
    394                    .map(|line| format!("🔒 {}", line.trim()))
    395                    .collect();
    396 
    397                if lines.is_empty() {
    398                    Ok(ChatOpResult::Message(format!("🔒 Could not retrieve SSL certificate for {}", domain)))
    399                } else {
    400                    Ok(ChatOpResult::Block(lines))
    401                }
    402            }
    403            Err(_) => Ok(ChatOpResult::Message(format!("🔒 SSL check not available. Try: https://www.ssllabs.com/ssltest/analyze.html?d={}", domain))),
    404        }
    405    }
    406 }
    407 
    408 /// Tor exit node checker
    409 pub struct TorCheckCommand;
    410 
    411 impl ChatCommand for TorCheckCommand {
    412    fn name(&self) -> &'static str {
    413        "torcheck"
    414    }
    415    fn description(&self) -> &'static str {
    416        "Check if IP is a Tor exit node"
    417    }
    418    fn usage(&self) -> &'static str {
    419        "/torcheck <ip>"
    420    }
    421 
    422    fn execute(
    423        &self,
    424        args: Vec<String>,
    425        _context: &CommandContext,
    426    ) -> Result<ChatOpResult, ChatOpError> {
    427        if args.is_empty() {
    428            return Err(ChatOpError::MissingArguments(
    429                "Please specify an IP address".to_string(),
    430            ));
    431        }
    432 
    433        let ip = &args[0];
    434 
    435        // Basic IP validation
    436        if !ip.chars().all(|c| c.is_ascii_digit() || c == '.') || ip.split('.').count() != 4 {
    437            return Err(ChatOpError::InvalidSyntax(
    438                "Please provide a valid IPv4 address".to_string(),
    439            ));
    440        }
    441 
    442        // Try checking with a Tor exit list service
    443        match Command::new("curl")
    444            .args(&["-s", "--max-time", "5", &format!("https://check.torproject.org/torbulkexitlist?ip={}", ip)])
    445            .output()
    446        {
    447            Ok(output) => {
    448                if output.status.success() {
    449                    let result = String::from_utf8_lossy(&output.stdout);
    450                    if result.contains(ip) {
    451                        Ok(ChatOpResult::Message(format!("🧅 {} is a Tor exit node", ip)))
    452                    } else {
    453                        Ok(ChatOpResult::Message(format!("🧅 {} is not a known Tor exit node", ip)))
    454                    }
    455                } else {
    456                    Ok(ChatOpResult::Message(format!("🧅 Could not check Tor status for {}", ip)))
    457                }
    458            }
    459            Err(_) => Ok(ChatOpResult::Message(format!("🧅 Tor check not available. Try: https://metrics.torproject.org/exonerator.html?ip={}", ip))),
    460        }
    461    }
    462 }