use std::{borrow::Cow, net::IpAddr};
use http::{HeaderName, Method};
use super::compile::{Field, Group, Test, TextTest};
pub(crate) trait Request {
fn method(&self) -> &Method;
fn host(&self) -> &str;
fn path(&self) -> &str;
fn query(&self) -> Option<&str>;
fn header(&self, name: &HeaderName) -> Option<Cow<'_, str>>;
fn client(&self) -> Option<IpAddr>;
}
pub(super) fn evaluate_one(field: &Field, test: &Test, request: &impl Request) -> bool {
match (field, test) {
(Field::Method, Test::Method { methods, negate }) => {
methods.contains(request.method()) != *negate
}
(Field::Client, Test::Client { networks, negate }) => {
let listed = client(request)
.is_some_and(|client| networks.iter().any(|network| network.contains(client)));
listed != *negate
}
(Field::Host, Test::Text { test, negate }) => {
let host = lowercase(request.host());
text_test(test, Some(&host)) != *negate
}
(Field::Path, Test::Text { test, negate }) => {
let path = crate::routing::paths::normalize_path(request.path());
text_test(test, Some(&path)) != *negate
}
_ => false,
}
}
fn client(request: &impl Request) -> Option<u128> {
request.client().map(|ip| match ip {
IpAddr::V4(v4) => u128::from(v4.to_ipv6_mapped()),
IpAddr::V6(v6) => u128::from(v6),
})
}
pub(super) fn lowercase(host: &str) -> Cow<'_, str> {
if host.bytes().any(|byte| byte.is_ascii_uppercase()) {
Cow::Owned(host.to_ascii_lowercase())
} else {
Cow::Borrowed(host)
}
}
fn text_test(test: &TextTest, value: Option<&str>) -> bool {
match (test, value) {
(TextTest::Exists(exists), value) => value.is_some() == *exists,
(TextTest::Matches, _) | (_, None) => false,
(TextTest::Eq(expected), Some(value)) => value == expected,
(TextTest::In(expected), Some(value)) => expected.iter().any(|expected| value == expected),
(TextTest::Prefix(prefix), Some(value)) => value.starts_with(prefix.as_str()),
(TextTest::Suffix(suffix), Some(value)) => value.ends_with(suffix.as_str()),
(TextTest::Contains(part), Some(value)) => value.contains(part.as_str()),
(TextTest::Wildcard(parts), Some(value)) => wildcard(parts, value),
}
}
pub(super) fn evaluate(group: &Group, request: &impl Request, mut set: impl FnMut(u32, bool)) {
match &group.field {
Field::Method => {
let method = request.method();
for (index, test) in &group.tests {
if let Test::Method { methods, negate } = test {
set(*index, methods.contains(method) != *negate);
}
}
}
Field::Client => {
let client = client(request);
for (index, test) in &group.tests {
if let Test::Client { networks, negate } = test {
let listed = client.is_some_and(|client| {
networks.iter().any(|network| network.contains(client))
});
set(*index, listed != *negate);
}
}
}
Field::Host => {
let host = lowercase(request.host());
text_tests(group, Some(&host), set);
}
Field::Path => {
let path = crate::routing::paths::normalize_path(request.path());
text_tests(group, Some(&path), set);
}
Field::Query(name) => text_tests(group, query(request.query(), name).as_deref(), set),
Field::Header(name) => text_tests(group, request.header(name).as_deref(), set),
Field::Cookie(name) => {
let cookies = request.header(&http::header::COOKIE);
text_tests(group, cookie(cookies.as_deref(), name), set);
}
}
}
fn text_tests(group: &Group, value: Option<&str>, mut set: impl FnMut(u32, bool)) {
for (index, test) in &group.tests {
let Test::Text { test, negate } = test else {
continue;
};
if matches!(test, TextTest::Matches) {
continue;
}
set(*index, text_test(test, value) != *negate);
}
if let Some((patterns, tests)) = &group.regexes {
let matched = value
.filter(|value| patterns.is_match(value))
.map(|value| patterns.matches(value));
for (pattern, index) in tests.iter().enumerate() {
set(
*index,
matched
.as_ref()
.is_some_and(|matched| matched.matched(pattern)),
);
}
}
}
fn wildcard(parts: &[String], value: &str) -> bool {
let (Some(first), Some(last)) = (parts.first(), parts.last()) else {
return false;
};
if parts.len() == 1 {
return value == first;
}
let Some(mut rest) = value.strip_prefix(first.as_str()) else {
return false;
};
let Some(between) = rest.strip_suffix(last.as_str()) else {
return false;
};
rest = between;
for part in parts.get(1..parts.len() - 1).unwrap_or_default() {
match rest
.find(part.as_str())
.and_then(|at| rest.get(at + part.len()..))
{
Some(after) => rest = after,
None => return false,
}
}
true
}
fn query<'a>(query: Option<&'a str>, name: &str) -> Option<Cow<'a, str>> {
query?.split('&').find_map(|pair| {
let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
(decode(key) == name).then(|| decode(value))
})
}
fn decode(text: &str) -> Cow<'_, str> {
if !text.contains(['%', '+']) {
return Cow::Borrowed(text);
}
let mut decoded = Vec::with_capacity(text.len());
let mut rest = text.as_bytes();
while let Some((&byte, tail)) = rest.split_first() {
rest = tail;
match byte {
b'+' => decoded.push(b' '),
b'%' => {
let hex = rest
.get(..2)
.and_then(|hex| std::str::from_utf8(hex).ok())
.and_then(|hex| u8::from_str_radix(hex, 16).ok());
match hex {
Some(byte) => {
decoded.push(byte);
rest = rest.get(2..).unwrap_or_default();
}
None => decoded.push(b'%'),
}
}
byte => decoded.push(byte),
}
}
Cow::Owned(String::from_utf8_lossy(&decoded).into_owned())
}
fn cookie<'a>(cookies: Option<&'a str>, name: &str) -> Option<&'a str> {
cookies?.split(';').find_map(|pair| {
let (key, value) = pair.trim().split_once('=')?;
(key == name).then_some(value)
})
}
#[cfg(test)]
mod tests {
use super::{cookie, decode, query, wildcard};
fn parts(pattern: &str) -> Vec<String> {
pattern.split('*').map(ToString::to_string).collect()
}
#[test]
fn test_wildcard() {
assert!(wildcard(&parts("/api/*"), "/api/users"));
assert!(wildcard(&parts("/api/*"), "/api/"));
assert!(!wildcard(&parts("/api/*"), "/apis"));
assert!(wildcard(&parts("*.example.com"), "a.b.example.com"));
assert!(!wildcard(&parts("*.example.com"), "example.com"));
assert!(wildcard(&parts("/a/*/c/*"), "/a/b/c/d"));
assert!(!wildcard(&parts("/a/*/c"), "/a/b/d"));
assert!(wildcard(&parts("*"), "anything"));
assert!(wildcard(&parts("/exact"), "/exact"));
assert!(!wildcard(&parts("ab*ba"), "aba"));
}
#[test]
fn test_query_and_cookies() {
assert_eq!(
query(Some("a=1&q=a+b%21&q=2"), "q").as_deref(),
Some("a b!")
);
assert_eq!(query(Some("flag&b=2"), "flag").as_deref(), Some(""));
assert_eq!(query(Some("a=1"), "b"), None);
assert_eq!(query(None, "b"), None);
assert_eq!(decode("100%"), "100%");
assert_eq!(decode("%zz"), "%zz");
assert_eq!(cookie(Some("a=1; beta=1; c=3"), "beta"), Some("1"));
assert_eq!(cookie(Some("betas=1"), "beta"), None);
}
}