use anyhow::Result; use http::Request; use hyper::body::Incoming; use regex::Regex; use std::collections::HashMap; use tracing::debug; /// Request matcher system for complex routing logic #[derive(Debug, Clone)] pub enum Matcher { /// Match request path Path(PathMatcher), /// Match HTTP method Method(MethodMatcher), /// Match request headers Header(HeaderMatcher), /// Match query parameters Query(QueryMatcher), /// Match remote IP/CIDR RemoteIP(RemoteIPMatcher), /// Logical NOT matcher Not(Box), /// Logical AND matcher (all must match) And(Vec), /// Logical OR matcher (any must match) Or(Vec), } impl Matcher { /// Check if this matcher matches the given request pub fn matches(&self, req: &Request, remote_addr: std::net::SocketAddr) -> bool { match self { Matcher::Path(matcher) => matcher.matches(req), Matcher::Method(matcher) => matcher.matches(req), Matcher::Header(matcher) => matcher.matches(req), Matcher::Query(matcher) => matcher.matches(req), Matcher::RemoteIP(matcher) => matcher.matches(remote_addr), Matcher::Not(inner) => !inner.matches(req, remote_addr), Matcher::And(matchers) => matchers.iter().all(|m| m.matches(req, remote_addr)), Matcher::Or(matchers) => matchers.iter().any(|m| m.matches(req, remote_addr)), } } } /// Path matching with glob patterns and exact matches #[derive(Debug, Clone)] pub struct PathMatcher { patterns: Vec, } #[derive(Debug, Clone)] enum PathPattern { Exact(String), Prefix(String), Suffix(String), Glob(String), Regex(Regex), } impl PathMatcher { pub fn new(patterns: Vec) -> Result { let mut compiled_patterns = Vec::new(); for pattern in patterns { let compiled = if pattern.contains('*') || pattern.contains('?') { // Convert glob to regex let regex_pattern = pattern .replace(".", "\\.") .replace("*", ".*") .replace("?", "."); let regex = Regex::new(&format!("^{}$", regex_pattern))?; PathPattern::Regex(regex) } else if pattern.starts_with('/') && pattern.ends_with('*') { // Prefix match let prefix = pattern.trim_end_matches('*').to_string(); PathPattern::Prefix(prefix) } else if pattern.starts_with('*') && pattern.ends_with('/') { // Suffix match (unusual but possible) let suffix = pattern.trim_start_matches('*').to_string(); PathPattern::Suffix(suffix) } else { // Exact match PathPattern::Exact(pattern) }; compiled_patterns.push(compiled); } Ok(Self { patterns: compiled_patterns, }) } pub fn matches(&self, req: &Request) -> bool { let path = req.uri().path(); for pattern in &self.patterns { let matches = match pattern { PathPattern::Exact(exact) => path == exact, PathPattern::Prefix(prefix) => path.starts_with(prefix), PathPattern::Suffix(suffix) => path.ends_with(suffix), PathPattern::Glob(_) => false, // Converted to regex PathPattern::Regex(regex) => regex.is_match(path), }; if matches { debug!("Path '{}' matched pattern: {:?}", path, pattern); return true; } } false } } /// HTTP method matching #[derive(Debug, Clone)] pub struct MethodMatcher { methods: Vec, } impl MethodMatcher { pub fn new(methods: Vec) -> Result { let parsed_methods: Result, _> = methods .into_iter() .map(|m| m.parse::()) .collect(); Ok(Self { methods: parsed_methods?, }) } pub fn matches(&self, req: &Request) -> bool { self.methods.contains(req.method()) } } /// Header matching with exact values or patterns #[derive(Debug, Clone)] pub struct HeaderMatcher { conditions: Vec, } #[derive(Debug, Clone)] struct HeaderCondition { name: String, matcher: HeaderValueMatcher, } #[derive(Debug, Clone)] enum HeaderValueMatcher { Exact(String), Contains(String), Regex(Regex), Exists, } impl HeaderMatcher { pub fn new(conditions: Vec<(String, Option)>) -> Result { let mut parsed_conditions = Vec::new(); for (name, value) in conditions { let matcher = match value { None => HeaderValueMatcher::Exists, Some(v) if v.starts_with("~") => { // Regex pattern let pattern = &v[1..]; let regex = Regex::new(pattern)?; HeaderValueMatcher::Regex(regex) } Some(v) if v.contains('*') => { // Convert simple glob to contains check for now let contains = v.replace('*', ""); HeaderValueMatcher::Contains(contains) } Some(v) => HeaderValueMatcher::Exact(v), }; parsed_conditions.push(HeaderCondition { name: name.to_lowercase(), matcher, }); } Ok(Self { conditions: parsed_conditions, }) } pub fn matches(&self, req: &Request) -> bool { for condition in &self.conditions { let header_value = req.headers() .get(&condition.name) .and_then(|v| v.to_str().ok()); let matches = match &condition.matcher { HeaderValueMatcher::Exists => header_value.is_some(), HeaderValueMatcher::Exact(expected) => { header_value.map(|v| v == expected).unwrap_or(false) } HeaderValueMatcher::Contains(substring) => { header_value.map(|v| v.contains(substring)).unwrap_or(false) } HeaderValueMatcher::Regex(regex) => { header_value.map(|v| regex.is_match(v)).unwrap_or(false) } }; if !matches { return false; } } true } } /// Query parameter matching #[derive(Debug, Clone)] pub struct QueryMatcher { conditions: HashMap>, } impl QueryMatcher { pub fn new(conditions: HashMap>) -> Self { Self { conditions } } pub fn matches(&self, req: &Request) -> bool { let query = req.uri().query().unwrap_or(""); let query_params: HashMap = url::form_urlencoded::parse(query.as_bytes()) .map(|(k, v)| (k.to_string(), v.to_string())) .collect(); for (key, expected_value) in &self.conditions { match expected_value { None => { // Just check if parameter exists if !query_params.contains_key(key) { return false; } } Some(expected) => { // Check parameter value match query_params.get(key) { Some(actual) if actual == expected => continue, _ => return false, } } } } true } } /// Remote IP/CIDR matching #[derive(Debug, Clone)] pub struct RemoteIPMatcher { allowed_ranges: Vec, } impl RemoteIPMatcher { pub fn new(ranges: Vec) -> Result { let mut parsed_ranges = Vec::new(); for range in ranges { if range.contains('/') { // CIDR notation parsed_ranges.push(range.parse()?); } else { // Single IP - convert to /32 or /128 let ip: std::net::IpAddr = range.parse()?; let net = match ip { std::net::IpAddr::V4(ipv4) => ipnet::IpNet::V4(ipnet::Ipv4Net::new(ipv4, 32)?), std::net::IpAddr::V6(ipv6) => ipnet::IpNet::V6(ipnet::Ipv6Net::new(ipv6, 128)?), }; parsed_ranges.push(net); } } Ok(Self { allowed_ranges: parsed_ranges, }) } pub fn matches(&self, remote_addr: std::net::SocketAddr) -> bool { let ip = remote_addr.ip(); for range in &self.allowed_ranges { if range.contains(&ip) { return true; } } false } } /// Named matcher sets for reuse (like Caddy's @name syntax) #[derive(Debug, Clone)] pub struct NamedMatcher { pub name: String, pub matcher: Matcher, } /// Collection of matchers with named references #[derive(Debug, Default)] pub struct MatcherSet { named_matchers: HashMap, } impl MatcherSet { pub fn new() -> Self { Self::default() } /// Add a named matcher pub fn add_named_matcher(&mut self, name: String, matcher: Matcher) { self.named_matchers.insert(name, matcher); } /// Get a named matcher by reference pub fn get_matcher(&self, name: &str) -> Option<&Matcher> { self.named_matchers.get(name) } /// Check if a named matcher matches the request pub fn matches(&self, name: &str, req: &Request, remote_addr: std::net::SocketAddr) -> bool { match self.get_matcher(name) { Some(matcher) => matcher.matches(req, remote_addr), None => { debug!("Named matcher '{}' not found", name); false } } } } #[cfg(test)] mod tests { use super::*; use http::{Method, Request}; use hyper::body::Incoming; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; fn create_test_request(method: Method, path: &str) -> Request { Request::builder() .method(method) .uri(path) .body(Incoming::default()) .unwrap() } fn test_addr() -> SocketAddr { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 8080) } #[test] fn test_exact_path_matcher() { let matcher = PathMatcher::new(vec!["/api/v1/users".to_string()]).unwrap(); let req1 = create_test_request(Method::GET, "/api/v1/users"); let req2 = create_test_request(Method::GET, "/api/v1/posts"); assert!(matcher.matches(&req1)); assert!(!matcher.matches(&req2)); } #[test] fn test_prefix_path_matcher() { let matcher = PathMatcher::new(vec!["/admin*".to_string()]).unwrap(); let req1 = create_test_request(Method::GET, "/admin/dashboard"); let req2 = create_test_request(Method::GET, "/api/admin"); assert!(matcher.matches(&req1)); assert!(!matcher.matches(&req2)); } #[test] fn test_method_matcher() { let matcher = MethodMatcher::new(vec!["GET".to_string(), "POST".to_string()]).unwrap(); let req1 = create_test_request(Method::GET, "/test"); let req2 = create_test_request(Method::POST, "/test"); let req3 = create_test_request(Method::DELETE, "/test"); assert!(matcher.matches(&req1)); assert!(matcher.matches(&req2)); assert!(!matcher.matches(&req3)); } #[test] fn test_header_matcher() { let conditions = vec![ ("content-type".to_string(), Some("application/json".to_string())), ("authorization".to_string(), None), // Just check existence ]; let matcher = HeaderMatcher::new(conditions).unwrap(); let req = Request::builder() .header("content-type", "application/json") .header("authorization", "Bearer token") .body(Incoming::default()) .unwrap(); assert!(matcher.matches(&req)); let req_missing_auth = Request::builder() .header("content-type", "application/json") .body(Incoming::default()) .unwrap(); assert!(!matcher.matches(&req_missing_auth)); } #[test] fn test_query_matcher() { let mut conditions = HashMap::new(); conditions.insert("format".to_string(), Some("json".to_string())); conditions.insert("debug".to_string(), None); // Just check existence let matcher = QueryMatcher::new(conditions); let req = create_test_request(Method::GET, "/api?format=json&debug=1"); assert!(matcher.matches(&req)); let req_no_debug = create_test_request(Method::GET, "/api?format=json"); assert!(!matcher.matches(&req_no_debug)); } #[test] fn test_remote_ip_matcher() { let matcher = RemoteIPMatcher::new(vec![ "192.168.1.0/24".to_string(), "10.0.0.1".to_string(), ]).unwrap(); let addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 8080); let addr2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 8080); let addr3 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1)), 8080); assert!(matcher.matches(addr1)); assert!(matcher.matches(addr2)); assert!(!matcher.matches(addr3)); } #[test] fn test_not_matcher() { let path_matcher = PathMatcher::new(vec!["/admin*".to_string()]).unwrap(); let not_matcher = Matcher::Not(Box::new(Matcher::Path(path_matcher))); let req1 = create_test_request(Method::GET, "/admin/dashboard"); let req2 = create_test_request(Method::GET, "/api/users"); assert!(!not_matcher.matches(&req1, test_addr())); assert!(not_matcher.matches(&req2, test_addr())); } #[test] fn test_and_matcher() { let path_matcher = PathMatcher::new(vec!["/api*".to_string()]).unwrap(); let method_matcher = MethodMatcher::new(vec!["GET".to_string()]).unwrap(); let and_matcher = Matcher::And(vec![ Matcher::Path(path_matcher), Matcher::Method(method_matcher), ]); let req1 = create_test_request(Method::GET, "/api/users"); let req2 = create_test_request(Method::POST, "/api/users"); let req3 = create_test_request(Method::GET, "/dashboard"); assert!(and_matcher.matches(&req1, test_addr())); assert!(!and_matcher.matches(&req2, test_addr())); assert!(!and_matcher.matches(&req3, test_addr())); } #[test] fn test_named_matcher_set() { let mut matcher_set = MatcherSet::new(); // Add a named matcher like Caddy's @not_admin let not_admin_matcher = Matcher::Not(Box::new(Matcher::Path( PathMatcher::new(vec!["/admin*".to_string()]).unwrap() ))); matcher_set.add_named_matcher("not_admin".to_string(), not_admin_matcher); let req1 = create_test_request(Method::GET, "/admin/dashboard"); let req2 = create_test_request(Method::GET, "/api/users"); assert!(!matcher_set.matches("not_admin", &req1, test_addr())); assert!(matcher_set.matches("not_admin", &req2, test_addr())); } }