use std::time::Duration;
pub(crate) fn recaptcha_audio_client(timeout: Duration) -> reqwest::Client {
reqwest::Client::builder()
.timeout(timeout)
.no_gzip()
.no_brotli()
.default_headers(recaptcha_audio_headers())
.build()
.expect("reCAPTCHA audio reqwest client must build with rustls-tls enabled")
}
pub(crate) fn recaptcha_audio_headers() -> reqwest::header::HeaderMap {
guise::http::default_browser_request_header_map_without_compression(
guise::http::BrowserRequestKind::AudioSubresource,
)
.expect("canonical stealth audio-subresource headers must be valid")
}
pub(crate) fn recaptcha_audio_cookie_header(
cookies: &[crate::cookies::CapturedCookie],
audio_url: &reqwest::Url,
) -> Option<reqwest::header::HeaderValue> {
let pairs = cookies
.iter()
.filter(|cookie| cookie_matches_url(cookie, audio_url))
.filter(|cookie| cookie_pair_is_header_safe(&cookie.name, &cookie.value))
.map(|cookie| format!("{}={}", cookie.name, cookie.value))
.collect::<Vec<_>>();
if pairs.is_empty() {
return None;
}
reqwest::header::HeaderValue::from_str(&pairs.join("; ")).ok()
}
pub(crate) fn cookie_matches_url(
cookie: &crate::cookies::CapturedCookie,
url: &reqwest::Url,
) -> bool {
if cookie.is_expired_now() {
return false;
}
if cookie.secure && url.scheme() != "https" {
return false;
}
let Some(host) = url.host_str() else {
return false;
};
domain_matches(&cookie.domain, host) && path_matches(&cookie.path, url.path())
}
pub(crate) fn domain_matches(cookie_domain: &str, host: &str) -> bool {
let cookie_domain = cookie_domain.trim().to_ascii_lowercase();
if cookie_domain.is_empty() {
return false;
}
let host = host.to_ascii_lowercase();
let Some(domain) = cookie_domain.strip_prefix('.') else {
return host == cookie_domain;
};
host == domain || host.ends_with(&format!(".{domain}"))
}
pub(crate) fn path_matches(cookie_path: &str, request_path: &str) -> bool {
let cookie_path = if cookie_path.is_empty() {
"/"
} else {
cookie_path
};
if cookie_path == "/" || request_path == cookie_path {
return true;
}
request_path.starts_with(cookie_path)
&& (cookie_path.ends_with('/')
|| request_path
.as_bytes()
.get(cookie_path.len())
.is_some_and(|byte| *byte == b'/'))
}
pub(crate) fn cookie_pair_is_header_safe(name: &str, value: &str) -> bool {
!name.is_empty()
&& name
.bytes()
.all(|byte| byte.is_ascii_graphic() && !matches!(byte, b'=' | b';' | b',' | b'"'))
&& value
.bytes()
.all(|byte| byte.is_ascii_graphic() && !matches!(byte, b';'))
}