use crate::Page;
use anyhow::{Context, Result};
use async_trait::async_trait;
use serde::Deserialize;
use super::{parse_detection_result, CaptchaInfo, DetectedCaptcha, Detector};
use crate::provider::CaptchaProvider;
use crate::solver::{CaptchaType, SolveMethod};
#[derive(Debug, Clone, Deserialize)]
pub struct ProviderRule {
pub name: String,
pub priority: i32,
#[serde(default = "default_solver_methods")]
pub solver_methods: Vec<SolveMethod>,
#[serde(default)]
pub solver_names: Vec<String>,
pub triggers: Triggers,
}
fn default_solver_methods() -> Vec<SolveMethod> {
vec![
SolveMethod::BehavioralBypass,
SolveMethod::ThirdPartyService,
]
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct Triggers {
#[serde(default)]
pub selectors: Vec<String>,
#[serde(default)]
pub window_globals: Vec<String>,
#[serde(default)]
pub script_src_contains: Vec<String>,
#[serde(default)]
pub title_contains: Vec<String>,
#[serde(default)]
pub cookie_names: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct RuleSet {
#[serde(default, rename = "provider")]
pub providers: Vec<ProviderRule>,
}
pub const BUILT_IN_RULES_TOML: &str = include_str!("../../rules/community.toml");
pub fn built_in_rules() -> Result<RuleSet> {
RuleSet::parse(BUILT_IN_RULES_TOML)
}
impl RuleSet {
pub fn parse(input: &str) -> Result<Self> {
toml::from_str(input).context("parsing TOML rules")
}
pub fn load_from_path(path: impl AsRef<std::path::Path>) -> Result<Self> {
let text = std::fs::read_to_string(path.as_ref())
.with_context(|| format!("reading {}", path.as_ref().display()))?;
Self::parse(&text)
}
pub fn into_detectors(self) -> Vec<Box<dyn Detector>> {
self.providers
.into_iter()
.map(|r| Box::new(RuleDetector::from(r)) as Box<dyn Detector>)
.collect()
}
}
pub struct RuleDetector {
rule: ProviderRule,
js: String,
leaked_name: &'static str,
leaked_methods: &'static [SolveMethod],
leaked_names: &'static [&'static str],
}
impl From<ProviderRule> for RuleDetector {
fn from(rule: ProviderRule) -> Self {
let js = build_probe_js(&rule);
let leaked_name: &'static str = Box::leak(rule.name.clone().into_boxed_str());
let leaked_methods: &'static [SolveMethod] =
Box::leak(rule.solver_methods.clone().into_boxed_slice());
let name_refs: Vec<&'static str> = rule
.solver_names
.iter()
.map(|n| -> &'static str { Box::leak(n.clone().into_boxed_str()) })
.collect();
let leaked_names: &'static [&'static str] = Box::leak(name_refs.into_boxed_slice());
Self {
rule,
js,
leaked_name,
leaked_methods,
leaked_names,
}
}
}
impl RuleDetector {
pub fn rule(&self) -> &ProviderRule {
&self.rule
}
pub fn js(&self) -> &str {
&self.js
}
}
#[async_trait]
impl Detector for RuleDetector {
fn name(&self) -> &'static str {
self.leaked_name
}
fn priority(&self) -> i32 {
self.rule.priority
}
async fn detect(&self, page: &Page) -> Result<Option<CaptchaInfo>> {
let raw = page.evaluate(self.js.as_str()).await?;
let val = raw.into_value::<serde_json::Value>()?;
Ok(parse_detection_result(val))
}
}
impl CaptchaProvider for RuleDetector {
fn name(&self) -> &'static str {
<Self as Detector>::name(self)
}
fn detected_kind(&self) -> DetectedCaptcha {
DetectedCaptcha::Custom(self.rule.name.clone())
}
fn captcha_type(&self) -> CaptchaType {
CaptchaType::Custom(self.rule.name.clone())
}
fn recommended_solver_methods(&self) -> &'static [SolveMethod] {
self.leaked_methods
}
fn recommended_solver_names(&self) -> &'static [&'static str] {
self.leaked_names
}
fn detector(&self) -> &dyn Detector {
self
}
}
fn build_probe_js(rule: &ProviderRule) -> String {
let kind_str = json_str(&format!("custom:{}", rule.name));
let container_str = json_str("(rule-derived)");
let mut body = String::from("(function(){");
for sel in &rule.triggers.selectors {
let sel_lit = json_str(sel);
body.push_str(&format!(
"if(document.querySelector({sel_lit})){{return {{kind:{kind_str},site_key:null,container:{container_str}}};}}",
));
}
for global in &rule.triggers.window_globals {
let g_lit = json_str(global);
body.push_str(&format!(
"if(typeof window[{g_lit}]!=='undefined'){{return {{kind:{kind_str},site_key:null,container:{container_str}}};}}",
));
}
if !rule.triggers.script_src_contains.is_empty() {
body.push_str("var __cfg_scripts=document.querySelectorAll('script[src]');");
body.push_str("for(var __i=0;__i<__cfg_scripts.length;__i++){var __src=__cfg_scripts[__i].getAttribute('src')||'';");
for sub in &rule.triggers.script_src_contains {
let s_lit = json_str(sub);
body.push_str(&format!(
"if(__src.indexOf({s_lit})>=0){{return {{kind:{kind_str},site_key:null,container:{container_str}}};}}",
));
}
body.push('}');
}
if !rule.triggers.title_contains.is_empty() {
body.push_str("var __cfg_title=(document.title||'').toLowerCase();");
for sub in &rule.triggers.title_contains {
let s_lit = json_str(&sub.to_lowercase());
body.push_str(&format!(
"if(__cfg_title.indexOf({s_lit})>=0){{return {{kind:{kind_str},site_key:null,container:{container_str}}};}}",
));
}
}
if !rule.triggers.cookie_names.is_empty() {
body.push_str("var __cfg_cookies=(document.cookie||'').split(';').map(function(c){return c.trim().split('=')[0]||'';});");
for cookie in &rule.triggers.cookie_names {
let c_lit = json_str(cookie);
body.push_str(&format!(
"if(__cfg_cookies.indexOf({c_lit})>=0){{return {{kind:{kind_str},site_key:null,container:{container_str}}};}}",
));
}
}
body.push_str("return {kind:'none',site_key:null,container:null};})()");
body
}
fn json_str(s: &str) -> String {
serde_json::to_string(s).expect("serde_json never fails on &str")
}
#[cfg(test)]
#[path = "rules/tests.rs"]
mod tests;