#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChallengeKind {
None,
IuamV1,
JsChallenge,
Turnstile,
AccessDenied,
RateLimited,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Confidence {
Low,
Medium,
High,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChallengeSignal {
pub kind: ChallengeKind,
pub confidence: Confidence,
pub evidence: Vec<&'static str>,
}
impl ChallengeSignal {
pub fn none() -> Self {
Self {
kind: ChallengeKind::None,
confidence: Confidence::High,
evidence: Vec::new(),
}
}
pub fn is_challenge(&self) -> bool {
!matches!(self.kind, ChallengeKind::None)
}
}
#[derive(Debug, Clone, Copy)]
pub struct DetectionInput<'a> {
pub status: Option<u16>,
pub server: Option<&'a str>,
pub cf_mitigated: Option<&'a str>,
pub cf_ray: Option<&'a str>,
pub body: &'a str,
}
impl<'a> DetectionInput<'a> {
pub fn from_body(body: &'a str) -> Self {
Self {
status: None,
server: None,
cf_mitigated: None,
cf_ray: None,
body,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn none_signal_is_clean() {
let signal = ChallengeSignal::none();
assert_eq!(signal.kind, ChallengeKind::None);
assert_eq!(signal.confidence, Confidence::High);
assert!(signal.evidence.is_empty());
assert!(!signal.is_challenge());
}
#[test]
fn populated_signal_is_a_challenge() {
let signal = ChallengeSignal {
kind: ChallengeKind::Turnstile,
confidence: Confidence::High,
evidence: vec!["marker"],
};
assert!(signal.is_challenge());
}
#[test]
fn from_body_leaves_http_metadata_unset() {
let input = DetectionInput::from_body("<html></html>");
assert_eq!(input.body, "<html></html>");
assert!(input.status.is_none());
assert!(input.server.is_none());
assert!(input.cf_mitigated.is_none());
assert!(input.cf_ray.is_none());
}
#[test]
fn confidence_orders_low_to_high() {
assert!(Confidence::Low < Confidence::Medium);
assert!(Confidence::Medium < Confidence::High);
}
}