Skip to main content

leyline/core/
block.rs

1use std::sync::{Arc, LazyLock};
2
3use http::{HeaderName, StatusCode};
4use serde::Deserialize;
5
6use crate::core::error::{Error, Kind, Result};
7use crate::core::response::Response;
8
9const STATUS_VENDOR: &str = "status";
10const BUILTIN: &str = include_str!("../../profiles/blocks.toml");
11
12static BUILTIN_RULES: LazyLock<BlockRules> = LazyLock::new(|| {
13    BlockRules::from_toml(BUILTIN).expect("bundled block rules are statically valid")
14});
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
17#[serde(rename_all = "lowercase")]
18#[non_exhaustive]
19pub enum BlockKind {
20    Challenge,
21    Captcha,
22    Block,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26#[non_exhaustive]
27pub struct BlockSignal {
28    pub vendor: String,
29    pub kind: BlockKind,
30}
31
32#[derive(Debug, Clone, Default)]
33pub struct BlockRules {
34    rules: Arc<Vec<BlockRule>>,
35}
36
37#[derive(Debug, Clone)]
38struct BlockRule {
39    vendor: String,
40    kind: BlockKind,
41    status: Option<StatusCode>,
42    header: Option<HeaderName>,
43    value: Option<String>,
44}
45
46#[derive(Deserialize)]
47#[serde(deny_unknown_fields)]
48struct RulesFile {
49    #[serde(default)]
50    rule: Vec<RuleEntry>,
51}
52
53#[derive(Deserialize)]
54#[serde(deny_unknown_fields)]
55struct RuleEntry {
56    vendor: String,
57    kind: BlockKind,
58    status: Option<u16>,
59    header: Option<String>,
60    value: Option<String>,
61}
62
63impl BlockRules {
64    pub fn builtin() -> &'static BlockRules {
65        &BUILTIN_RULES
66    }
67
68    pub fn from_toml(source: &str) -> Result<Self> {
69        let file: RulesFile =
70            toml::from_str(source).map_err(|e| Error::new(Kind::Config).with_source(e))?;
71        let rules = file
72            .rule
73            .into_iter()
74            .map(BlockRule::try_from)
75            .collect::<Result<Vec<_>>>()?;
76        Ok(Self {
77            rules: Arc::new(rules),
78        })
79    }
80
81    pub fn statuses(statuses: impl IntoIterator<Item = u16>) -> Self {
82        let rules = statuses
83            .into_iter()
84            .filter_map(|code| StatusCode::from_u16(code).ok())
85            .map(|status| BlockRule {
86                vendor: STATUS_VENDOR.to_string(),
87                kind: BlockKind::Block,
88                status: Some(status),
89                header: None,
90                value: None,
91            })
92            .collect();
93        Self {
94            rules: Arc::new(rules),
95        }
96    }
97
98    pub fn extend(&mut self, other: BlockRules) {
99        Arc::make_mut(&mut self.rules).extend(other.rules.iter().cloned());
100    }
101
102    pub fn check(&self, response: &Response) -> Option<BlockSignal> {
103        self.rules
104            .iter()
105            .find(|rule| rule.matches(response))
106            .map(|rule| BlockSignal {
107                vendor: rule.vendor.clone(),
108                kind: rule.kind,
109            })
110    }
111}
112
113impl TryFrom<RuleEntry> for BlockRule {
114    type Error = Error;
115
116    fn try_from(entry: RuleEntry) -> Result<Self> {
117        let invalid = |message: String| Error::new(Kind::Config).with_message(message);
118        if entry.status.is_none() && entry.header.is_none() {
119            return Err(invalid(format!(
120                "block rule for {} needs a status or a header",
121                entry.vendor
122            )));
123        }
124        if entry.value.is_some() && entry.header.is_none() {
125            return Err(invalid(format!(
126                "block rule for {} has a value without a header",
127                entry.vendor
128            )));
129        }
130        let status = entry
131            .status
132            .map(StatusCode::from_u16)
133            .transpose()
134            .map_err(|e| Error::new(Kind::Config).with_source(e))?;
135        let header = entry
136            .header
137            .map(|name| HeaderName::from_bytes(name.as_bytes()))
138            .transpose()
139            .map_err(|e| Error::new(Kind::Config).with_source(e))?;
140        Ok(Self {
141            vendor: entry.vendor,
142            kind: entry.kind,
143            status,
144            header,
145            value: entry.value,
146        })
147    }
148}
149
150impl BlockRule {
151    fn matches(&self, response: &Response) -> bool {
152        if self
153            .status
154            .is_some_and(|status| status != response.status())
155        {
156            return false;
157        }
158        let Some(header) = &self.header else {
159            return true;
160        };
161        let mut values = response.headers().get_all(header).iter().peekable();
162        let Some(expected) = &self.value else {
163            return values.peek().is_some();
164        };
165        values
166            .filter_map(|value| value.to_str().ok())
167            .any(|value| value.trim().eq_ignore_ascii_case(expected))
168    }
169}