use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy)]
pub struct ProbeResult {
pub avg_latency: Duration,
pub samples: u32,
}
impl ProbeResult {
pub fn required_vus(&self, target_rps: u32, num_operations: u32) -> u32 {
let lat_secs = self.avg_latency.as_secs_f64().max(0.001);
let ops = num_operations.max(1) as f64;
let raw = (target_rps as f64 * ops * lat_secs).ceil() as u32;
raw.saturating_add(1).max(1)
}
}
pub async fn probe_target_latency(
target: &str,
samples: u32,
skip_tls_verify: bool,
) -> Option<ProbeResult> {
probe_target_latency_guarded(target, samples, skip_tls_verify, crate::ssrf::process_guard())
.await
}
pub async fn probe_target_latency_guarded(
target: &str,
samples: u32,
skip_tls_verify: bool,
guard: Option<crate::ssrf::Policy>,
) -> Option<ProbeResult> {
if let Some(policy) = guard {
let parsed = url::Url::parse(target).ok()?;
crate::ssrf::check_url(&parsed, policy).ok()?;
}
let client = crate::ssrf::client_builder_for(guard)
.timeout(Duration::from_secs(5))
.danger_accept_invalid_certs(skip_tls_verify)
.build()
.ok()?;
let mut total = Duration::ZERO;
let mut count: u32 = 0;
for _ in 0..samples {
let start = Instant::now();
let head = client.head(target).send().await;
let elapsed = match head {
Ok(_) => start.elapsed(),
Err(_) => {
let start = Instant::now();
match client.get(target).send().await {
Ok(_) => start.elapsed(),
Err(_) => continue,
}
}
};
total += elapsed;
count = count.saturating_add(1);
}
if count == 0 {
return None;
}
Some(ProbeResult {
avg_latency: total / count,
samples: count,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn required_vus_scales_with_latency() {
let fast = ProbeResult {
avg_latency: Duration::from_millis(2),
samples: 3,
};
assert_eq!(fast.required_vus(1000, 1), 3);
let slow = ProbeResult {
avg_latency: Duration::from_millis(100),
samples: 3,
};
assert_eq!(slow.required_vus(100, 1), 11);
}
#[test]
fn required_vus_scales_with_operation_count() {
let probe = ProbeResult {
avg_latency: Duration::from_millis(15),
samples: 3,
};
assert_eq!(probe.required_vus(100, 1), 3); assert_eq!(probe.required_vus(100, 12), 19); }
#[test]
fn required_vus_clamps_to_one() {
let fast = ProbeResult {
avg_latency: Duration::from_micros(50),
samples: 1,
};
assert!(fast.required_vus(1, 1) >= 1);
}
#[test]
fn required_vus_treats_zero_operations_as_one() {
let probe = ProbeResult {
avg_latency: Duration::from_millis(10),
samples: 1,
};
assert_eq!(probe.required_vus(100, 0), probe.required_vus(100, 1));
}
#[tokio::test]
async fn probe_returns_none_for_unreachable() {
let result = probe_target_latency("http://127.0.0.1:1/", 1, false).await;
assert!(result.is_none());
}
#[tokio::test]
async fn guarded_probe_never_reaches_an_internal_target() {
use crate::ssrf::{test_server, Policy};
use std::sync::atomic::Ordering;
let (port, hits) = test_server::serve(test_server::OK).await;
for target in [
format!("http://127.0.0.1:{port}/"),
format!("http://localhost:{port}/"),
] {
let r = probe_target_latency_guarded(&target, 2, false, Some(Policy::strict())).await;
assert!(r.is_none(), "{target}");
}
assert_eq!(hits.load(Ordering::SeqCst), 0);
let (inner, inner_hits) = test_server::serve(test_server::OK).await;
let (outer, _) =
test_server::serve(test_server::redirect_to(&format!("http://0.0.0.0:{inner}/"))).await;
let outer_url = format!("http://127.0.0.1:{outer}/");
let r = probe_target_latency_guarded(&outer_url, 1, false, Some(Policy::for_test())).await;
assert!(r.is_none());
assert_eq!(inner_hits.load(Ordering::SeqCst), 0);
probe_target_latency_guarded(&outer_url, 1, false, None).await;
assert!(inner_hits.load(Ordering::SeqCst) > 0);
let r = probe_target_latency_guarded(&format!("http://127.0.0.1:{port}/"), 1, false, None)
.await;
assert!(r.is_some());
}
}