use std::sync::Arc; use std::time::Duration; use futures::stream::{self, StreamExt}; use once_cell::sync::Lazy; use regex::Regex; use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Emitter}; use tokio::sync::Semaphore; #[derive(Debug, Clone, Deserialize)] pub struct HttpProbeRequest { pub targets: Vec, pub ports: Option>, pub concurrency: usize, pub timeout_ms: u64, pub follow_redirects: bool, } #[derive(Debug, Clone, Serialize)] pub struct HttpProbeResult { pub url: String, pub status: u16, pub title: Option, pub server: Option, pub content_length: Option, pub tech: Vec, } static TITLE_RE: Lazy = Lazy::new(|| Regex::new(r"(?is)]*>(.*?)").unwrap()); fn detect_tech(headers: &reqwest::header::HeaderMap, body: &str) -> Vec { let mut tech = Vec::new(); let get = |k: &str| headers.get(k).and_then(|v| v.to_str().ok()).unwrap_or(""); let server = get("server").to_lowercase(); let powered = get("x-powered-by").to_lowercase(); if server.contains("nginx") { tech.push("nginx".into()); } if server.contains("apache") { tech.push("apache".into()); } if server.contains("cloudflare") { tech.push("cloudflare".into()); } if server.contains("iis") { tech.push("iis".into()); } if powered.contains("php") { tech.push("php".into()); } if powered.contains("express") { tech.push("express".into()); } if powered.contains("asp.net") { tech.push("asp.net".into()); } let b = body.to_lowercase(); if b.contains("wp-content") || b.contains("wp-includes") { tech.push("wordpress".into()); } if b.contains("drupal-settings-json") { tech.push("drupal".into()); } if b.contains("joomla") { tech.push("joomla".into()); } if b.contains("__next_data__") { tech.push("next.js".into()); } if b.contains("data-reactroot") || b.contains("__reactcontainer") { tech.push("react".into()); } if b.contains("ng-version") { tech.push("angular".into()); } if b.contains("window.laravel") || b.contains("laravel_session") { tech.push("laravel".into()); } tech } async fn probe_one(client: &reqwest::Client, url: String) -> Option { let resp = client.get(&url).send().await.ok()?; let status = resp.status().as_u16(); let headers = resp.headers().clone(); let content_length = resp.content_length(); let server = headers .get("server") .and_then(|v| v.to_str().ok()) .map(String::from); let body = resp.text().await.unwrap_or_default(); let title = TITLE_RE .captures(&body) .and_then(|c| c.get(1)) .map(|m| m.as_str().trim().to_string()) .filter(|s| !s.is_empty()); let tech = detect_tech(&headers, &body); Some(HttpProbeResult { url, status, title, server, content_length, tech }) } #[tauri::command] pub async fn http_probe( app: AppHandle, req: HttpProbeRequest, ) -> Result, String> { let client = reqwest::Client::builder() .danger_accept_invalid_certs(true) .timeout(Duration::from_millis(req.timeout_ms)) .redirect(if req.follow_redirects { reqwest::redirect::Policy::limited(5) } else { reqwest::redirect::Policy::none() }) .user_agent("Mozilla/5.0 (PocketPentester)") .build() .map_err(|e| e.to_string())?; let ports = req.ports.unwrap_or_else(|| vec![80, 443, 8080, 8443]); let mut urls: Vec = Vec::new(); for t in &req.targets { let t = t.trim(); if t.starts_with("http://") || t.starts_with("https://") { urls.push(t.to_string()); continue; } for p in &ports { let scheme = if matches!(p, 443 | 8443) { "https" } else { "http" }; if *p == 80 || *p == 443 { urls.push(format!("{scheme}://{t}")); } else { urls.push(format!("{scheme}://{t}:{p}")); } } } let total = urls.len(); let sem = Arc::new(Semaphore::new(req.concurrency.max(1))); let done = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let client = Arc::new(client); let results: Vec = stream::iter(urls.into_iter()) .map(|url| { let sem = sem.clone(); let done = done.clone(); let client = client.clone(); let app = app.clone(); async move { let _permit = sem.acquire().await.unwrap(); let res = probe_one(&client, url).await; let n = done.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; let _ = app.emit("httpx:progress", serde_json::json!({"done": n, "total": total})); if let Some(ref r) = res { let _ = app.emit("httpx:hit", r.clone()); } res } }) .buffer_unordered(req.concurrency.max(1)) .filter_map(|x| async move { x }) .collect() .await; let _ = app.emit("httpx:done", results.len()); Ok(results) }