dsad
This commit is contained in:
147
src-tauri/src/modules/httpx.rs
Normal file
147
src-tauri/src/modules/httpx.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
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<String>,
|
||||
pub ports: Option<Vec<u16>>,
|
||||
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<String>,
|
||||
pub server: Option<String>,
|
||||
pub content_length: Option<u64>,
|
||||
pub tech: Vec<String>,
|
||||
}
|
||||
|
||||
static TITLE_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"(?is)<title[^>]*>(.*?)</title>").unwrap());
|
||||
|
||||
fn detect_tech(headers: &reqwest::header::HeaderMap, body: &str) -> Vec<String> {
|
||||
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<HttpProbeResult> {
|
||||
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<Vec<HttpProbeResult>, 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<String> = 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<HttpProbeResult> = 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)
|
||||
}
|
||||
Reference in New Issue
Block a user