#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChallengeKind {
Vercel,
Cloudflare,
}
impl ChallengeKind {
pub fn provider(self) -> &'static str {
match self {
ChallengeKind::Vercel => "Vercel",
ChallengeKind::Cloudflare => "Cloudflare",
}
}
}
pub fn detect(status: u16, headers: &[(String, String)]) -> Option<ChallengeKind> {
let header = |name: &str| -> Option<&str> {
headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_str())
};
if header("x-vercel-mitigated").is_some_and(|v| v.eq_ignore_ascii_case("challenge"))
|| header("x-vercel-challenge-token").is_some()
{
return Some(ChallengeKind::Vercel);
}
if header("cf-mitigated").is_some_and(|v| v.eq_ignore_ascii_case("challenge")) {
return Some(ChallengeKind::Cloudflare);
}
let _ = status;
None
}
#[cfg(test)]
mod tests {
use super::*;
fn h(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn detects_vercel_mitigated_header() {
let headers = h(&[("x-vercel-mitigated", "challenge"), ("server", "Vercel")]);
assert_eq!(detect(429, &headers), Some(ChallengeKind::Vercel));
}
#[test]
fn detects_vercel_via_challenge_token() {
let headers = h(&[("x-vercel-challenge-token", "2.1782...")]);
assert_eq!(detect(429, &headers), Some(ChallengeKind::Vercel));
}
#[test]
fn header_name_match_is_case_insensitive() {
let headers = h(&[("X-Vercel-Mitigated", "Challenge")]);
assert_eq!(detect(429, &headers), Some(ChallengeKind::Vercel));
}
#[test]
fn detects_cloudflare_mitigated_header() {
let headers = h(&[("cf-mitigated", "challenge"), ("server", "cloudflare")]);
assert_eq!(detect(403, &headers), Some(ChallengeKind::Cloudflare));
}
#[test]
fn plain_429_without_markers_is_not_a_challenge() {
let headers = h(&[("retry-after", "30"), ("server", "nginx")]);
assert_eq!(detect(429, &headers), None);
}
#[test]
fn cf_mitigated_non_challenge_value_is_ignored() {
let headers = h(&[("cf-mitigated", "block")]);
assert_eq!(detect(403, &headers), None);
}
#[test]
fn provider_names() {
assert_eq!(ChallengeKind::Vercel.provider(), "Vercel");
assert_eq!(ChallengeKind::Cloudflare.provider(), "Cloudflare");
}
}