pub mod aws;
pub use aws::{AwsWafError, ChallengeAlgorithmMap, GokuContext, SolvedChallenge};
#[derive(Debug, thiserror::Error)]
pub enum WafError {
#[error("waf: no challenge detected")]
NoChallenge,
#[error("waf: aws replay failed: {0}")]
Aws(#[from] AwsWafError),
#[error("waf: vendor not implemented: {0}")]
NotImplemented(&'static str),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChallengeKind {
AwsWaf(Box<GokuContext>),
Cloudflare,
DataDome,
}
#[must_use]
pub fn detect_challenge<'a, I>(html: &str, headers: I) -> Option<ChallengeKind>
where
I: IntoIterator<Item = (&'a str, &'a str)>,
{
let mut aws_by_header = false;
for (name, value) in headers {
if name.eq_ignore_ascii_case("x-amzn-waf-action")
&& (value.eq_ignore_ascii_case("challenge") || value.eq_ignore_ascii_case("captcha"))
{
aws_by_header = true;
break;
}
}
if (aws_by_header || html.contains("awswaf.com"))
&& let Some(ctx) = aws::extract_goku_props(html)
{
return Some(ChallengeKind::AwsWaf(Box::new(ctx)));
}
if html.contains("challenges.cloudflare.com") || html.contains("cf-turnstile") {
return Some(ChallengeKind::Cloudflare);
}
if html.contains(".datadome.co") || html.contains("dd_cookie_test") {
return Some(ChallengeKind::DataDome);
}
None
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cookie {
pub name: String,
pub value: String,
pub domain: String,
}
pub fn solve_replay(kind: &ChallengeKind) -> Result<SolvedChallenge, WafError> {
match kind {
ChallengeKind::AwsWaf(ctx) => Ok(aws::solve_replay(ctx)?),
ChallengeKind::Cloudflare => Err(WafError::NotImplemented("cloudflare")),
ChallengeKind::DataDome => Err(WafError::NotImplemented("datadome")),
}
}
#[cfg(test)]
mod tests {
use super::{ChallengeKind, detect_challenge, solve_replay};
const AWS_FIXTURE: &str = r#"
<html><head>
<script src="https://abc123.awswaf.com/x/y/challenge.js"></script>
<script>
window.gokuProps = {
"challenge": "deadbeef",
"challengeType": "deadbeefcafebabe1234567890abcdef1234567890abcdefdeadbeefcafebabe"
};
</script>
</head></html>
"#;
#[test]
fn detects_aws_from_header() {
let headers = [("x-amzn-waf-action", "challenge")];
let kind = detect_challenge(AWS_FIXTURE, headers).expect("aws detected");
assert!(matches!(kind, ChallengeKind::AwsWaf(_)));
}
#[test]
fn detects_aws_from_body() {
let kind = detect_challenge(AWS_FIXTURE, std::iter::empty::<(&str, &str)>())
.expect("aws detected");
assert!(matches!(kind, ChallengeKind::AwsWaf(_)));
}
#[test]
fn detects_cloudflare_turnstile() {
let html =
r#"<script src="https://challenges.cloudflare.com/cdn-cgi/challenge/..."></script>"#;
let kind = detect_challenge(html, std::iter::empty::<(&str, &str)>())
.expect("cloudflare detected");
assert!(matches!(kind, ChallengeKind::Cloudflare));
}
#[test]
fn detects_datadome() {
let html = r#"<script src="https://js.datadome.co/boot.js"></script>"#;
let kind =
detect_challenge(html, std::iter::empty::<(&str, &str)>()).expect("datadome detected");
assert!(matches!(kind, ChallengeKind::DataDome));
}
#[test]
fn ignores_clean_html() {
let html = "<html><body><h1>hi</h1></body></html>";
assert!(detect_challenge(html, std::iter::empty::<(&str, &str)>()).is_none());
}
#[test]
fn solve_replay_for_aws_returns_solution() {
let kind = detect_challenge(AWS_FIXTURE, std::iter::empty::<(&str, &str)>())
.expect("aws detected");
let solved = solve_replay(&kind).expect("solver succeeds for mp_verify");
assert_eq!(solved.algo, "mp_verify_network_bandwidth");
}
}