132 lines
4.1 KiB
Rust
132 lines
4.1 KiB
Rust
// ===================================================================
|
|
// 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(" ")
|
|
}
|