0.34 #2

Merged
gru merged 16 commits from 0.34 into master 2026-10-18 12:01:51 +02:00
14 changed files with 198 additions and 57 deletions
Showing only changes of commit 9f20d5195e - Show all commits

No files matched your search

+13 -13
View File
@@ -326,7 +326,7 @@ b45d6040810c93ae2acb04ef6b0a59c189751edf9008cd81a4521c9e04e7f07d src/docker.rs
4c2c988191a97f81568a8df64af772463d010844f1abf327871901cdb32d4336 src/docker/model.rs
5f4610c59b2d4ff6a205a2d9ae11bc84be689b2a68302459f546b3f43e53593c src/docker/reconcile.rs
a12c47df4b90b6fc36c978a42477e6ddffbbbf69aed7252101c2fd829709bd6e src/docker/tests.rs
61cad5d547c4aa56017d7a7ab80f356670fdd4f5b232dc04a50dbb5969645dac src/forwarding.rs
2ac87cc55b1eaeb32e8abd3b1f891321bdd20bcc134cf2ea7dbe9154f4799597 src/forwarding.rs
9c712ed59cfca15b79c3339a031c8903c83468c9d3b5e29f7e59d9a222b97561 src/forwarding/peer_formatting.rs
0259def26930308047a248548ff911f64638b43bdfe7f765ae8c82a87a6aab21 src/forwarding/tests.rs
af7a4e91c32ffbe4701c8c06e48ba3f8bbbe55e8b9e877aecb30ccdf858e9e8f src/geoip.rs
@@ -359,9 +359,9 @@ fc8e99eac83c083d65101f6402aacbcc8392d66508c31566a50de3ebcb12e7da src/main/admin
0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs
e5c2e9189777725f4afa1c3c052e4f75047ec3f2f486630d990f5d3c194bcc96 src/main/tasks.rs
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
78aa428a3ed8f9a11d69ec2a6783679fb27ea99a74bc18d866c65c5391d61293 src/metrics/vhost.rs
e9cd8d9aff1de7c92b3381f7646e5c577f857d960bbb276e0736b29973b97319 src/metrics/vhost/tests.rs
068436f6c5a10d5e6bd1b69299bf99690bfd70da2a9fe5fd0de57c32b982bd22 src/metrics/global.rs
f8a9e1eb9466ca71bd70259248cd96354507d568408f5ec07cd60ce33ba15393 src/metrics/vhost.rs
c24bbdf13406112b6494ae68748bee63077767ecd528f663f32996e5ebd2b407 src/metrics/vhost/tests.rs
a4702c95c7e5077c2ccff86e2e3216e1da6ff9ee4383ae199a0aff2f78db325f src/not_found.rs
c915d30bbd7032d867e7916b4cd45f3bb7e7604d3f4558f4383c59394b4300c6 src/not_found/tests.rs
d66936cc15fd5d56566d40fa8cab93f80d94b05f10ac44478a30f63572a95e32 src/pages.rs
@@ -376,16 +376,16 @@ e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus
fb2d8d45cd2e947f783eb981b37fa418c39fcb327a3af17a648fadea7f37f888 src/prometheus/web_php.rs
2c97367686b54989573ce7fb81b2c6dde72adb254df9522bc05ded74f1fbffb6 src/proxy/body.rs
66b7f2cb47f2e70dc26b7b19eb8650872e34d3e44cf250d2effdb758295f8b8a src/proxy/body/tests.rs
7586fc88e10381eb97f3be6a66f355d16088cc9481f50c193eee8fe169c4fc8e src/proxy/cache_helpers.rs
1c0ba20c9c3327d76468d3e70f82c8e53659bce166a119de82020a9f8631dc2f src/proxy/cache_helpers.rs
aa49c9928bb91e3d5bae0d340d88396a0417b9edec6192d41e1403533319a897 src/proxy/cache_http_invalidation.rs
676c1b325fd8e55bcdd86451c7d1d8b8d347ca00e8da6b3e5b148a58c376a9b4 src/proxy/cache_plan.rs
3404b03096862de1050088f62cdd811ea1ab61d191f902501173107f81f23b9b src/proxy/cache_plan/tests.rs
a8848cbdf85368548b003af2b43823c453866c472af4c7d2ff0ec7da14c44c8d src/proxy/error_fallback.rs
f2f9cae1c74eaf66fc365c0709a6eb64e0b27457d4a00728e02cff8a641a1295 src/proxy/forward/cache_lookup.rs
7ee9ee1663d739a7361d97806b4cdbaf07fc0aab13c70a2c4a9804cff76cfb9c src/proxy/forward/cache_lookup.rs
cfa941b60b5d167ec9d8e2566ea3342b588d1a9078a6ca711ad6062ddf00fec9 src/proxy/forward/response.rs
44e1bae0a35ddae6a084c1d50b936fd10dd51059f331525b758447b526a2bddf src/proxy/forward/route_cache.rs
691b54a0cc33089e67d61ffc715b5c015976bae3fbd453b26cb06764be2482e5 src/proxy/forward/success_finish.rs
0a921474ef2b93eabdaf3888651cf29130eace5ccb1e0f5d6bc25e6aa69280be src/proxy/forward/success_prepare.rs
39332a8691946191a10b3061128d699ba768bad54045bdbc93266b6bce16b327 src/proxy/forward/success_finish.rs
269082bb95d796decbdf71c1ce8c7cd08ef5efd0d6c6375ae57c90fd48595d25 src/proxy/forward/success_prepare.rs
3f21439bbe4966fff43b3ebce8b59fa76ecce376197b4c56a2dce3a6d98f5556 src/proxy/forward/upstream.rs
bb8c9117e05b0cb4bbfe029ef4d1a031fc755cdce255b15dad072afd1c845737 src/proxy/forward/web_source.rs
3b9f27445c637dbf093b07a5b4e77cbff4df2bab2ad622bb87b6772dab245ec6 src/proxy/forward_entry.rs
@@ -397,7 +397,7 @@ cc7fe83f42acb19d79abe83107427cdc6b0ed14083702f4fe66af22f7cdfc4cc src/proxy/head
55133ceace2e5262d9c54fa41bea099a27b817d1e09c1cd0e2493fd9cee4da38 src/proxy/redirect_helpers.rs
2280841494c3e14ccc5e3f02b026fe3c4328a3dab04ab449b9286bb69e39c028 src/proxy/request_id.rs
b9a2307c6aa7f16613d935508a61574dfee964020b1b5be86293df2d684615d8 src/proxy/request_id/tests.rs
94a50c0bf147565098e06672df3625e44d5af34ef1f12fd355d82cc7a72cf033 src/proxy/tests/authority_tests.rs
de104d9aba11c155e8d72edbb4f37af193c08dc20fc4c4c956b3ffc323cf927a src/proxy/tests/authority_tests.rs
1aa9bbbfb2659facdaeaf185e11221b1c8cd5fe6e23c7c1dacd3aa55e3db4fcb src/proxy/tests/cache_fast_path_tests.rs
0f2eacadf1447db8aa1c1a78005aa262e0f747096bfe7e0a0e41a2cd31a0d17f src/proxy/tests/h3_navigation_tests.rs
d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/tests/http_invalidation_tests.rs
@@ -410,8 +410,8 @@ d866e7e72dee6ff54c2418c34e9899cbcd7e4e027d9cbe2b625044dc9062e4ee src/proxy/webs
7e9c4d4c207badabf90630223964bb2ad641f06d4149843cab751f192b8b4adf src/proxy/websocket/tests.rs
a44421714aab37f7ceda19e3ec2512c645d2a3ce5dcb3da5bc7a0d87a442d6a8 src/proxy/websocket/tunnel.rs
15fabd38576b61f650c039acdbf7ac5a5bd24330062475b94315285b2dc258b6 src/proxy/websocket/validation.rs
545247eb00b0116efbd4cc913cc251195a55feded1530e0d8d8945abbae78039 src/rate_limit.rs
e7e3a898e2c769b1ef11e58a13c1053104ada93dd694f8a03ea4b647c44c7d69 src/rate_limit/tests.rs
5dc44629340189e8a255992d33bbd78bbb6f2fdaf8a5e0c5bc3175d22c053c7d src/rate_limit.rs
57c879f573ebd10329ba64e2a4ae3776a400fe835a2bc417a157e4c41be514a8 src/rate_limit/tests.rs
d40ad63939097d2b91339127d5835a74b618d83b7ba31db0c512f05a82107d6e src/redirect.rs
811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 src/redirect/tests.rs
d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs
@@ -479,8 +479,8 @@ ca695e246bee5389b3757fe8588e48bff5d0ad473bc8588ad4ba7b52c99d7dfa src/upstream/c
56c8a9d1c801be0f41b35c75cb42716c46976951948009c8707122c0a6a1eb58 src/upstream/client_tests.rs
75fef0a488d3f3dabfa03c2eabc5995442dfb3c9a1fde8eb6653a124dda25d43 src/upstream/protocol_tests.rs
8a112154b9631f62a54584302a3ff6a64e4f177ce79aba678b4cb23e466c6ea3 src/upstream/tests.rs
a6b4905c245a22eafb1aa535d303d0fa24e4353db94e1d67fd42b825bf8c5adb src/user_agent.rs
6d7cb73de7d8c6f3780736480f5f81ef08813f4626fd5ed494aca0552cf214a1 src/user_agent/tests.rs
392f03936e176e2ba9a25edb2a273d09474e2c6801ed6fba8eed5bf1a62bd970 src/user_agent.rs
09ef411f04daa61bdcbcc395c5ba144a755f77a0402abfab636475fe6691b5ee src/user_agent/tests.rs
3aa1b07cd26d4a5b9e5e94d1d6a712ef1397d827e7f1e8c01f7195b98d2c5f67 src/vhost.rs
6678192de56bc10c5cf82bdff8da14eece7685f409592450c07478333aa3b99c src/vhost/error_fallback.rs
63e9fc7f7be78dc3e16a89f015a56a2da6ba3aff0197b70ff48321dfb17529bb src/vhost/model.rs
+14 -7
View File
@@ -100,7 +100,7 @@ fn cloudflare_peer(peer: SocketAddr) -> bool {
/// Return CF-Connecting-IP only when the TCP peer itself is in Cloudflare's
/// published edge ranges. The header is otherwise untrusted user input.
pub fn cloudflare_client_ip(headers: &HeaderMap, peer: SocketAddr) -> Option<IpAddr> {
if !cloudflare_peer(peer) {
if !headers.contains_key("cf-connecting-ip") || !cloudflare_peer(peer) {
return None;
}
let mut values = headers.get_all("cf-connecting-ip").iter();
@@ -120,7 +120,7 @@ pub fn cloudflare_client_ip(headers: &HeaderMap, peer: SocketAddr) -> Option<IpA
/// Return a trusted ISO country code supplied by Cloudflare. `CF-IPCountry` is
/// accepted only from a Cloudflare edge and only as one two-letter code.
pub fn cloudflare_country(headers: &HeaderMap, peer: SocketAddr) -> Option<String> {
if !cloudflare_peer(peer) {
if !headers.contains_key("cf-ipcountry") || !cloudflare_peer(peer) {
return None;
}
let mut values = headers.get_all("cf-ipcountry").iter();
@@ -319,11 +319,18 @@ impl Compiled {
values,
});
}
let mut ips = chain(incoming).unwrap_or_default();
ips.push(peer_ip);
// Discard everything left of the first untrusted hop walking from us.
let first = ips.iter().rposition(|ip| !self.trusted(*ip)).unwrap_or(0);
ips.drain(..first);
let mut proxy_chain = chain(incoming).unwrap_or_default();
// No forwarded hops: borrow the actual peer from the stack instead of
// allocating a one-element Vec on every trusted-proxy request.
let direct_peer = [peer_ip];
let ips: &[IpAddr] = if proxy_chain.is_empty() {
&direct_peer
} else {
proxy_chain.push(peer_ip);
// Discard untrusted upstream prefixes without shifting the Vec.
let first = proxy_chain.iter().rposition(|ip| !self.trusted(*ip)).unwrap_or(0);
&proxy_chain[first..]
};
let client_ip = ips[0];
let mut scheme = if tls { "https" } else { "http" };
if trusted_peer
+11 -1
View File
@@ -1,5 +1,15 @@
use serde::Serialize;
use std::sync::atomic::{AtomicU64, Ordering};
/// Isolate a high-contention counter without spreading every request-level
/// atomic across a separate cache line. Existing read/write API is unchanged.
#[repr(align(64))]
#[derive(Default, Debug)]
pub struct CacheAlignedAtomicU64(AtomicU64);
impl std::ops::Deref for CacheAlignedAtomicU64 {
type Target = AtomicU64;
fn deref(&self) -> &Self::Target { &self.0 }
}
#[derive(Default)]
pub struct Metrics {
pub admin_requests: AtomicU64,
@@ -35,7 +45,7 @@ pub struct Metrics {
pub websocket_bytes_in: AtomicU64,
pub websocket_bytes_out: AtomicU64,
pub websocket_duration_us: AtomicU64,
pub requests: AtomicU64,
pub requests: CacheAlignedAtomicU64,
pub completed: AtomicU64,
pub active: AtomicU64,
pub errors: AtomicU64,
+1 -1
View File
@@ -8,7 +8,7 @@ pub struct VhostMetrics {
pub bytes_in: AtomicU64,
pub bytes_out: AtomicU64,
pub duration_us: AtomicU64,
pub ewma_duration_us: AtomicU64,
pub ewma_duration_us: CacheAlignedAtomicU64,
pub status: [AtomicU64; 5],
pub buckets: [AtomicU64; 11],
}
+14
View File
@@ -63,4 +63,18 @@ mod compression_counter_tests {
assert_eq!(view.compression_zstd, 1);
assert_eq!(view.compression_deflate, 1);
}
#[test]
fn cache_aligned_counters_keep_exact_values_and_separate_hot_lines() {
let m = VhostMetrics::default();
let ewma = &m.ewma_duration_us as *const _ as usize;
assert_eq!(ewma % 64, 0);
m.active.fetch_add(1, Ordering::Relaxed);
m.requests.fetch_add(1, Ordering::Relaxed);
m.complete(200, false, std::time::Duration::from_micros(100), 20, 1040);
assert_eq!(n(&m.completed), 1);
assert_eq!(n(&m.active), 0);
assert_eq!(n(&m.requests), 1);
assert_eq!(n(&m.bytes_out), 1040);
assert_eq!(n(&m.ewma_duration_us), 100);
}
}
+16
View File
@@ -282,6 +282,22 @@ fn finish_headers(
crate::http3::advertise(response.headers_mut(), local, tls, active);
}
}
fn attach_origin_diagnostics(
response: &mut Response,
frontend: Option<&crate::config::CompiledFrontend>,
elapsed: Duration,
backend_id: &str,
) {
if let Some(frontend) = frontend {
if frontend.timing_headers || frontend.backend_timing_header {
response.extensions_mut().insert(OriginTiming(elapsed));
}
if frontend.backend_header {
response.extensions_mut().insert(OriginBackend(backend_id.to_owned()));
}
}
}
fn apply_timing_headers(
response: &mut Response,
frontend: Option<&crate::config::CompiledFrontend>,
+1 -4
View File
@@ -179,10 +179,7 @@ let serve_stale = |reason: &'static str, elapsed: Duration, origin_backend: &str
hit.resident_age_seconds,
);
hit.response.extensions_mut().insert(ResponseRulesApplied);
hit.response.extensions_mut().insert(OriginTiming(elapsed));
hit.response
.extensions_mut()
.insert(OriginBackend(origin_backend.to_owned()));
attach_origin_diagnostics(&mut hit.response, route.frontend.as_deref(), elapsed, origin_backend);
state.cache.record(scope, &host, "HIT", reason);
if let Some(exchange) = &exchange {
exchange.cache_status.store(1, Ordering::Relaxed);
+2 -6
View File
@@ -73,12 +73,8 @@ if response.status() == StatusCode::NOT_MODIFIED && revalidation_sent {
revalidated = crate::cache::make_not_modified(revalidated);
}
revalidated.extensions_mut().insert(ResponseRulesApplied);
revalidated
.extensions_mut()
.insert(OriginTiming(upstream_started.elapsed()));
revalidated
.extensions_mut()
.insert(OriginBackend(active_backend.id.clone()));
attach_origin_diagnostics(&mut revalidated, route.frontend.as_deref(),
upstream_started.elapsed(), &active_backend.id);
if let Some(exchange) = &exchange {
exchange.cache_status.store(1, Ordering::Relaxed);
*exchange
+2 -6
View File
@@ -366,12 +366,8 @@ if intercept_origin_error {
);
}
response.extensions_mut().insert(ResponseRulesApplied);
response
.extensions_mut()
.insert(OriginTiming(upstream_started.elapsed()));
response
.extensions_mut()
.insert(OriginBackend(active_backend.id.clone()));
attach_origin_diagnostics(&mut response, route.frontend.as_deref(),
upstream_started.elapsed(), &active_backend.id);
if !range_requested && handshake.is_none() {
if let Some(settings) = transform_settings.filter(|settings| settings.enabled) {
let transform_started = Instant::now();
+28
View File
@@ -409,6 +409,34 @@ mod authority_tests {
assert!(std::str::from_utf8(detail.as_bytes()).is_ok());
}
#[test]
fn origin_diagnostics_are_created_only_for_enabled_frontend_features() {
let mut response = Response::new(Body::empty());
attach_origin_diagnostics(&mut response, None, Duration::from_millis(5), "origin-a");
assert!(response.extensions().get::<OriginTiming>().is_none());
assert!(response.extensions().get::<OriginBackend>().is_none());
for (timing, backend, backend_timing) in [
(false, false, false),
(true, false, false),
(false, true, false),
(false, false, true),
(true, true, true),
] {
let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
config.vhosts[0].timing_headers = timing;
config.vhosts[0].backend_header = backend;
config.vhosts[0].backend_timing_header = backend_timing;
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
let route = snapshot.select("localhost", "/").unwrap();
let mut response = Response::new(Body::empty());
attach_origin_diagnostics(&mut response, route.frontend.as_deref(),
Duration::from_millis(5), "origin-a");
assert_eq!(response.extensions().get::<OriginTiming>().is_some(),
timing || backend_timing);
assert_eq!(response.extensions().get::<OriginBackend>().is_some(), backend);
}
}
#[test]
fn backend_diagnostics_are_opt_in_and_strip_spoofed_values() {
let mut response = Response::new(Body::empty());
+12 -7
View File
@@ -211,13 +211,18 @@ impl Settings {
self.country_access.allows(country)
}
fn effective(&self, country: Option<&str>, user_agent: Option<&str>) -> Option<(Limit, Option<usize>)> {
// Matching only needs a bounded copy. Oversized headers cannot force
// unbounded work in the hot request path.
if let Some(agent) = user_agent.filter(|ua| ua.len() <= MAX_USER_AGENT_BYTES) {
let candidate = agent.to_ascii_lowercase();
if let Some((index, rule)) = self.user_agent_limits.iter().enumerate()
.find(|(_, rule)| crate::user_agent::wildcard_match(&candidate, &rule.pattern)) {
return Some((Limit { requests: rule.requests, window_seconds: rule.window_seconds, burst: rule.burst }, Some(index)));
// With no UA overrides the common request never allocates a lowercase
// copy (and oversized headers still skip matching entirely).
if !self.user_agent_limits.is_empty() {
if let Some(agent) = user_agent.filter(|ua| ua.len() <= MAX_USER_AGENT_BYTES) {
if let Some((index, rule)) = self.user_agent_limits.iter().enumerate()
.find(|(_, rule)| crate::user_agent::wildcard_match_case_insensitive(agent, &rule.pattern))
{
return Some((Limit {
requests: rule.requests, window_seconds: rule.window_seconds,
burst: rule.burst,
}, Some(index)));
}
}
}
if let Some(country) = country {
+28
View File
@@ -157,4 +157,32 @@ mod tests {
assert!(settings.validate().is_ok());
}
#[test]
fn no_ua_rules_do_not_create_buckets_when_base_limiter_is_off() {
let settings = Settings::default();
let limiter = Limiter::new();
let ip = "192.0.2.34".parse().unwrap();
for _ in 0..32 {
assert!(limiter.check_with_user_agent(1, "site", ip, None,
Some("Mozilla/5.0 (compatible; Requests)"), &settings).is_ok());
}
assert!(lock(&limiter.state).buckets.is_empty());
}
#[test]
fn rate_limit_ua_wildcards_preserve_case_insensitive_quotas() {
let mut settings = Settings { user_agent_limits: vec![UserAgentLimit {
pattern: "*Scanner*Bot".into(), requests: 1, window_seconds: 60,
burst: 0,
}], ..Default::default() };
settings.validate().unwrap();
let limiter = Limiter::new();
let ip = "192.0.2.53".parse().unwrap();
assert!(limiter.check_with_user_agent(1, "site", ip, None,
Some("ACME-SCANNER-XXbot"), &settings).is_ok());
assert!(limiter.check_with_user_agent(1, "site", ip, None,
Some("acme-scanner-XXBOT"), &settings).is_err());
assert!(limiter.check_with_user_agent(1, "site", ip, None,
Some("ordinary-browser"), &settings).is_ok());
}
}
+35 -12
View File
@@ -76,24 +76,47 @@ pub(crate) fn wildcard_match(candidate: &str, pattern: &str) -> bool {
}
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 mut found_any = false;
for part in pattern.split('*').filter(|part| !part.is_empty()) {
let Some(found) = candidate[cursor..].find(part) else {
return false;
};
if index == 0 && !starts_wild && found != 0 {
if !found_any && !starts_wild && found != 0 {
return false;
}
found_any = true;
cursor += found + part.len();
}
ends_wild || cursor == candidate.len()
!found_any || ends_wild || cursor == candidate.len()
}
/// Match normalized policy patterns against ASCII agents without allocating
/// a lowercased request value. Non-ASCII inputs retain the original matcher.
pub(crate) fn wildcard_match_case_insensitive(candidate: &str, pattern: &str) -> bool {
if !candidate.is_ascii() || !pattern.is_ascii() {
return wildcard_match(&candidate.to_ascii_lowercase(), pattern);
}
let text = candidate.as_bytes();
let pattern_bytes = pattern.as_bytes();
if pattern_bytes.is_empty() { return true; }
let find_part = |from: usize, part: &[u8]| -> Option<usize> {
text[from..].windows(part.len()).position(|window| window.eq_ignore_ascii_case(part))
};
if !pattern_bytes.contains(&b'*') {
return find_part(0, pattern_bytes).is_some();
}
let starts_wild = pattern_bytes.first() == Some(&b'*');
let ends_wild = pattern_bytes.last() == Some(&b'*');
let mut cursor = 0usize;
let mut found_any = false;
for part in pattern_bytes.split(|byte| *byte == b'*').filter(|part| !part.is_empty()) {
let Some(found) = find_part(cursor, part) else { return false; };
if !found_any && !starts_wild && found != 0 { return false; }
found_any = true;
cursor += found + part.len();
}
!found_any || ends_wild || cursor == text.len()
}
impl CompiledPolicy {
@@ -104,10 +127,10 @@ impl CompiledPolicy {
let Some(value) = headers.get(USER_AGENT) else {
return false;
};
let candidate = String::from_utf8_lossy(value.as_bytes()).to_ascii_lowercase();
let candidate = String::from_utf8_lossy(value.as_bytes());
self.patterns
.iter()
.any(|pattern| wildcard_match(&candidate, pattern))
.any(|pattern| wildcard_match_case_insensitive(&candidate, pattern))
}
}
+21
View File
@@ -51,3 +51,24 @@
.unwrap();
assert!(!enabled.blocks(&headers(None)));
}
#[test]
fn allocation_free_ascii_matcher_matches_original_policy() {
for candidate in ["", "Mozilla/5.0", "CURL/8.4.0", "AcmeScanner/1",
"abc-DEF-ghi", "aBcXDeFz", "Content:Token", "żółw TOKEN"] {
for pattern in ["*", "curl/*", "*scanner*", "abc*ghi", "abc*def*", "*def",
"token", "missing", "mozilla", "*x*d*f*", "żółw*"] {
let normalized = pattern.to_ascii_lowercase();
assert_eq!(
wildcard_match_case_insensitive(candidate, &normalized),
wildcard_match(&candidate.to_ascii_lowercase(), &normalized),
"{candidate:?} vs {pattern:?}",
);
}
}
let policy = Policy { enabled: true,
patterns: vec!["*bot*".into(), "curl/*".into()] }.compile().unwrap();
assert!(policy.blocks(&headers(Some("GOOD-BOT/1.0"))));
assert!(policy.blocks(&headers(Some("CURL/9.0"))));
assert!(!policy.blocks(&headers(Some("Mozilla/5.0"))));
}