#[cfg(feature = "tls-impersonate")]
use std::time::Duration;
#[cfg(any(feature = "tls-impersonate", test))]
use crate::detect::block_response;
pub(crate) const VALIDATION_BODY_CAP: usize = 64 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GateStatus {
Clear,
Blocked {
status: u16,
},
Inconclusive,
}
#[cfg(feature = "tls-impersonate")]
#[must_use]
pub fn default_tls_profile() -> scanclient::tls_impersonate::ImpersonateProfile {
scanclient::tls_impersonate::ImpersonateProfile::Chrome131
}
pub async fn preflight_get(url: &str) -> GateStatus {
#[cfg(feature = "tls-impersonate")]
{
return preflight_get_with_profile(url, default_tls_profile()).await;
}
#[cfg(not(feature = "tls-impersonate"))]
{
let _ = url;
GateStatus::Inconclusive
}
}
#[cfg(feature = "tls-impersonate")]
pub async fn preflight_get_with_profile(
url: &str,
profile: scanclient::tls_impersonate::ImpersonateProfile,
) -> GateStatus {
use scanclient::StealthClient;
let client = match StealthClient::with_timeout(profile, Duration::from_secs(15)) {
Ok(c) => c,
Err(_) => return GateStatus::Inconclusive,
};
let resp = match client.send("GET", url, &[], None, 64 * 1024).await {
Ok(r) => r,
Err(_) => return GateStatus::Inconclusive,
};
classify_gate_response(resp.status, &resp.body)
}
#[cfg(feature = "tls-impersonate")]
pub(crate) async fn send_validation(
method: &str,
url: &str,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
timeout: Duration,
) -> Result<(u16, Option<String>), ()> {
#[cfg(feature = "tls-impersonate")]
{
return send_validation_stealth(method, url, headers, body, timeout).await;
}
#[cfg(not(feature = "tls-impersonate"))]
{
let _ = (method, url, headers, body, timeout);
Err(())
}
}
#[cfg(feature = "tls-impersonate")]
async fn send_validation_stealth(
method: &str,
url: &str,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
timeout: Duration,
) -> Result<(u16, Option<String>), ()> {
use scanclient::StealthClient;
let client = StealthClient::with_timeout(default_tls_profile(), timeout).map_err(|_| ())?;
let resp = client
.send(method, url, &headers, body.as_deref(), VALIDATION_BODY_CAP)
.await
.map_err(|_| ())?;
let text = String::from_utf8_lossy(&resp.body).into_owned();
Ok((resp.status, Some(text)))
}
#[cfg(any(feature = "tls-impersonate", test))]
fn classify_gate_response(status: u16, body: &[u8]) -> GateStatus {
let text = String::from_utf8_lossy(body);
if block_response::http_response_indicates_block(status, Some(text.as_ref())) {
GateStatus::Blocked { status }
} else if (200..300).contains(&status) {
GateStatus::Clear
} else {
GateStatus::Blocked { status }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_clear_on_benign_200() {
assert_eq!(
classify_gate_response(200, b"<html>ok</html>"),
GateStatus::Clear
);
}
#[test]
fn classify_blocked_on_erroracle_phrase() {
assert!(matches!(
classify_gate_response(200, b"Sorry, your request has been blocked"),
GateStatus::Blocked { status: 200 }
));
}
#[test]
fn classify_blocked_on_403() {
assert!(matches!(
classify_gate_response(403, b""),
GateStatus::Blocked { status: 403 }
));
}
}