wifiscan 0.4.0

Wireless network scanner TUI with monitor mode, handshake capture, deauth, and evil twin
Documentation
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Instant;

use chrono::Local;

// ── Debug Logging ───────────────────────────────────────────────────────────

#[derive(Clone)]
pub struct DebugLog {
    inner: Option<Arc<Mutex<std::fs::File>>>,
}

impl DebugLog {
    pub fn new(enabled: bool, path: &str) -> Self {
        if enabled {
            match std::fs::File::create(path) {
                Ok(f) => {
                    eprintln!("[+] Debug logging to {}", path);
                    Self { inner: Some(Arc::new(Mutex::new(f))) }
                }
                Err(e) => {
                    eprintln!("[-] Failed to create debug log: {}", e);
                    Self { inner: None }
                }
            }
        } else {
            Self { inner: None }
        }
    }

    pub fn log(&self, msg: &str) {
        if let Some(ref f) = self.inner {
            if let Ok(mut file) = f.lock() {
                let ts = Local::now().format("%H:%M:%S%.3f");
                let _ = writeln!(file, "[{}] {}", ts, msg);
            }
        }
    }

    pub fn log_hex(&self, prefix: &str, data: &[u8], max_bytes: usize) {
        if self.inner.is_none() { return; }
        let len = data.len().min(max_bytes);
        let hex: String = data[..len].iter().map(|b| format!("{:02x}", b)).collect::<Vec<_>>().join(" ");
        let suffix = if data.len() > max_bytes { format!("... ({} bytes total)", data.len()) } else { String::new() };
        self.log(&format!("{}: {}{}", prefix, hex, suffix));
    }

    pub fn is_enabled(&self) -> bool {
        self.inner.is_some()
    }
}

// ── Pcap Dumper ─────────────────────────────────────────────────────────────

#[derive(Clone)]
pub struct PcapDumper {
    inner: Option<Arc<Mutex<std::fs::File>>>,
    start: Instant,
}

impl PcapDumper {
    pub fn new(path: Option<&PathBuf>) -> Self {
        match path {
            Some(p) => {
                match std::fs::File::create(p) {
                    Ok(mut f) => {
                        // pcap global header: LINKTYPE_IEEE802_11_RADIOTAP (127)
                        let hdr: [u8; 24] = [
                            0xd4, 0xc3, 0xb2, 0xa1, 0x02, 0x00, 0x04, 0x00,
                            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                            0xff, 0xff, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00,
                        ];
                        if f.write_all(&hdr).is_ok() {
                            eprintln!("[+] Dumping all packets to {}", p.display());
                            Self { inner: Some(Arc::new(Mutex::new(f))), start: Instant::now() }
                        } else {
                            Self { inner: None, start: Instant::now() }
                        }
                    }
                    Err(e) => {
                        eprintln!("[-] Failed to create pcap dump: {}", e);
                        Self { inner: None, start: Instant::now() }
                    }
                }
            }
            None => Self { inner: None, start: Instant::now() },
        }
    }

    pub fn write_packet(&self, raw: &[u8]) {
        if let Some(ref f) = self.inner {
            if let Ok(mut file) = f.lock() {
                let elapsed = self.start.elapsed();
                let ts_sec = elapsed.as_secs() as u32;
                let ts_usec = elapsed.subsec_micros();
                let len = raw.len() as u32;
                let mut hdr = [0u8; 16];
                hdr[0..4].copy_from_slice(&ts_sec.to_le_bytes());
                hdr[4..8].copy_from_slice(&ts_usec.to_le_bytes());
                hdr[8..12].copy_from_slice(&len.to_le_bytes());
                hdr[12..16].copy_from_slice(&len.to_le_bytes());
                let _ = file.write_all(&hdr);
                let _ = file.write_all(raw);
            }
        }
    }
}

// ── Global Singletons ───────────────────────────────────────────────────────

static DEBUG_LOG: OnceLock<DebugLog> = OnceLock::new();
static PCAP_DUMP: OnceLock<PcapDumper> = OnceLock::new();

pub fn init_debug(enabled: bool, path: &str) {
    let _ = DEBUG_LOG.set(DebugLog::new(enabled, path));
}

pub fn init_pcap_dump(path: Option<&PathBuf>) {
    let _ = PCAP_DUMP.set(PcapDumper::new(path));
}

pub fn dbg_log(msg: &str) {
    if let Some(log) = DEBUG_LOG.get() {
        log.log(msg);
    }
}

pub fn dbg_hex(prefix: &str, data: &[u8], max_bytes: usize) {
    if let Some(log) = DEBUG_LOG.get() {
        log.log_hex(prefix, data, max_bytes);
    }
}

pub fn dbg_enabled() -> bool {
    DEBUG_LOG.get().map(|l| l.is_enabled()).unwrap_or(false)
}

pub fn pcap_dump(raw: &[u8]) {
    if let Some(d) = PCAP_DUMP.get() {
        d.write_packet(raw);
    }
}