0.34.6_repo_cleanup
This commit is contained in:
1 parent
ce1a90397c
commit
133d2f0f91
273 files changed
+5969
-6926
No files matched your search
@@ -0,0 +1,111 @@
|
||||
use anyhow::{ensure, Result};
|
||||
use http::{header::USER_AGENT, HeaderMap};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const MAX_PATTERNS: usize = 128;
|
||||
const MAX_PATTERN_BYTES: usize = 256;
|
||||
const MAX_TOTAL_BYTES: usize = 16 * 1024;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Policy {
|
||||
pub enabled: bool,
|
||||
pub patterns: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CompiledPolicy {
|
||||
enabled: bool,
|
||||
patterns: Vec<String>,
|
||||
}
|
||||
|
||||
impl Policy {
|
||||
pub fn compile(&self) -> Result<CompiledPolicy> {
|
||||
ensure!(
|
||||
self.patterns.len() <= MAX_PATTERNS,
|
||||
"User-Agent blocklist supports at most {MAX_PATTERNS} patterns"
|
||||
);
|
||||
let mut patterns = Vec::with_capacity(self.patterns.len());
|
||||
let mut total = 0usize;
|
||||
for raw in &self.patterns {
|
||||
let pattern = raw.trim();
|
||||
ensure!(
|
||||
!pattern.is_empty()
|
||||
&& pattern.len() <= MAX_PATTERN_BYTES
|
||||
&& !pattern.chars().any(char::is_control),
|
||||
"User-Agent block patterns must be 1..{MAX_PATTERN_BYTES} bytes without controls"
|
||||
);
|
||||
total = total.saturating_add(pattern.len());
|
||||
ensure!(
|
||||
total <= MAX_TOTAL_BYTES,
|
||||
"User-Agent blocklist is too large"
|
||||
);
|
||||
let normalized = pattern.to_ascii_lowercase();
|
||||
if !patterns.contains(&normalized) {
|
||||
patterns.push(normalized);
|
||||
}
|
||||
}
|
||||
Ok(CompiledPolicy {
|
||||
enabled: self.enabled,
|
||||
patterns,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_pattern(raw: &str) -> Result<String> {
|
||||
let value = raw.trim();
|
||||
ensure!(
|
||||
!value.is_empty()
|
||||
&& value.len() <= MAX_PATTERN_BYTES
|
||||
&& !value.chars().any(char::is_control),
|
||||
"User-Agent patterns must be 1..{MAX_PATTERN_BYTES} bytes without controls"
|
||||
);
|
||||
Ok(value.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
pub(crate) fn wildcard_match(candidate: &str, pattern: &str) -> bool {
|
||||
if !pattern.contains('*') {
|
||||
return candidate.contains(pattern);
|
||||
}
|
||||
if pattern == "*" {
|
||||
return true;
|
||||
}
|
||||
let starts_wild = pattern.starts_with('*');
|
||||
let ends_wild = pattern.ends_with('*');
|
||||
let parts = pattern
|
||||
.split('*')
|
||||
.filter(|part| !part.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
if parts.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let mut cursor = 0usize;
|
||||
for (index, part) in parts.iter().enumerate() {
|
||||
let Some(found) = candidate[cursor..].find(part) else {
|
||||
return false;
|
||||
};
|
||||
if index == 0 && !starts_wild && found != 0 {
|
||||
return false;
|
||||
}
|
||||
cursor += found + part.len();
|
||||
}
|
||||
ends_wild || cursor == candidate.len()
|
||||
}
|
||||
|
||||
impl CompiledPolicy {
|
||||
pub fn blocks(&self, headers: &HeaderMap) -> bool {
|
||||
if !self.enabled || self.patterns.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let Some(value) = headers.get(USER_AGENT) else {
|
||||
return false;
|
||||
};
|
||||
let candidate = String::from_utf8_lossy(value.as_bytes()).to_ascii_lowercase();
|
||||
self.patterns
|
||||
.iter()
|
||||
.any(|pattern| wildcard_match(&candidate, pattern))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
Reference in new issue
Block a user