This commit is contained in:
taqin
2026-04-19 21:10:40 +07:00
parent 5fdd214fdc
commit 27381d4e37
211 changed files with 53571 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
// ===================================================================
// Repeater — Burp-style manual HTTP request sender.
// Full control: method, URL, headers, body. Returns full response.
// ===================================================================
use std::collections::HashMap;
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize)]
pub struct RepeaterRequest {
pub method: String,
pub url: String,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(default)]
pub body: Option<String>,
#[serde(default = "default_timeout")]
pub timeout_ms: u64,
#[serde(default)]
pub follow_redirects: bool,
#[serde(default)]
pub ignore_tls: bool,
}
fn default_timeout() -> u64 { 15_000 }
#[derive(Debug, Clone, Serialize)]
pub struct RepeaterResponse {
pub status: u16,
pub status_text: String,
pub http_version: String,
pub headers: Vec<(String, String)>,
pub body: String,
pub body_len: usize,
pub content_type: Option<String>,
pub time_ms: u128,
pub final_url: String,
pub redirected: bool,
pub is_text: bool,
}
#[tauri::command]
pub async fn repeater_send(req: RepeaterRequest) -> Result<RepeaterResponse, String> {
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(req.ignore_tls)
.timeout(Duration::from_millis(req.timeout_ms))
.redirect(if req.follow_redirects {
reqwest::redirect::Policy::limited(10)
} else {
reqwest::redirect::Policy::none()
})
.user_agent("Mozilla/5.0 (PocketPentester-Repeater)")
.build()
.map_err(|e| e.to_string())?;
let method = reqwest::Method::from_bytes(req.method.to_uppercase().as_bytes())
.map_err(|e| format!("bad method: {e}"))?;
let mut builder = client.request(method, &req.url);
for (k, v) in &req.headers {
if k.trim().is_empty() { continue; }
builder = builder.header(k.trim(), v);
}
if let Some(body) = &req.body {
if !body.is_empty() {
builder = builder.body(body.clone());
}
}
let start = Instant::now();
let resp = builder.send().await.map_err(|e| e.to_string())?;
let status = resp.status();
let version = format!("{:?}", resp.version());
let final_url = resp.url().to_string();
let redirected = final_url != req.url;
let headers: Vec<(String, String)> = resp.headers().iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect();
let content_type = resp.headers().get("content-type")
.and_then(|v| v.to_str().ok()).map(String::from);
let is_text = content_type.as_deref().map(|c| {
c.starts_with("text/") || c.contains("json") || c.contains("xml")
|| c.contains("javascript") || c.contains("html") || c.contains("form-urlencoded")
}).unwrap_or(true);
let body = resp.text().await.unwrap_or_default();
let elapsed = start.elapsed().as_millis();
Ok(RepeaterResponse {
status: status.as_u16(),
status_text: status.canonical_reason().unwrap_or("").into(),
http_version: version,
headers,
body_len: body.len(),
body,
content_type,
time_ms: elapsed,
final_url,
redirected,
is_text,
})
}
#[tauri::command]
pub fn repeater_to_curl(req: RepeaterRequest) -> String {
let mut parts: Vec<String> = vec!["curl".into(), "-i".into()];
if req.ignore_tls { parts.push("-k".into()); }
if req.follow_redirects { parts.push("-L".into()); }
if req.method.to_uppercase() != "GET" {
parts.push("-X".into());
parts.push(req.method.to_uppercase());
}
for (k, v) in &req.headers {
if k.trim().is_empty() { continue; }
parts.push("-H".into());
parts.push(format!("'{}: {}'", k.trim(), v.replace('\'', "'\\''")));
}
if let Some(body) = &req.body {
if !body.is_empty() {
parts.push("--data-raw".into());
parts.push(format!("'{}'", body.replace('\'', "'\\''")));
}
}
parts.push(format!("'{}'", req.url));
parts.join(" ")
}