Quantum/src/routing/matchers.rs
RTSDA fd12f35e6c Implement core Caddy replacement functionality
- Add comprehensive HTTP/1.1 and HTTP/2 server support
- Implement reverse proxy with load balancing
- Add static file serving with proper MIME types
- Create multi-port server management
- Add TLS/HTTPS support with SNI and ACME
- Implement authentication middleware framework
- Add advanced routing and matchers system
- Create file sync service foundation
- Add metrics collection and health monitoring
- Implement simple configuration format
- Successfully tested with production-equivalent config

Core features working:
- Reverse proxy to localhost:3000 ✓
- Static file serving ✓
- Multi-port configuration ✓
- CORS headers and security ✓
- Simple config format detection ✓

Ready for production testing as Caddy replacement.
2025-08-17 20:02:04 -04:00

498 lines
16 KiB
Rust

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<Matcher>),
/// Logical AND matcher (all must match)
And(Vec<Matcher>),
/// Logical OR matcher (any must match)
Or(Vec<Matcher>),
}
impl Matcher {
/// Check if this matcher matches the given request
pub fn matches(&self, req: &Request<Incoming>, 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<PathPattern>,
}
#[derive(Debug, Clone)]
enum PathPattern {
Exact(String),
Prefix(String),
Suffix(String),
Glob(String),
Regex(Regex),
}
impl PathMatcher {
pub fn new(patterns: Vec<String>) -> Result<Self> {
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<Incoming>) -> 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<http::Method>,
}
impl MethodMatcher {
pub fn new(methods: Vec<String>) -> Result<Self> {
let parsed_methods: Result<Vec<_>, _> = methods
.into_iter()
.map(|m| m.parse::<http::Method>())
.collect();
Ok(Self {
methods: parsed_methods?,
})
}
pub fn matches(&self, req: &Request<Incoming>) -> bool {
self.methods.contains(req.method())
}
}
/// Header matching with exact values or patterns
#[derive(Debug, Clone)]
pub struct HeaderMatcher {
conditions: Vec<HeaderCondition>,
}
#[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<String>)>) -> Result<Self> {
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<Incoming>) -> 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<String, Option<String>>,
}
impl QueryMatcher {
pub fn new(conditions: HashMap<String, Option<String>>) -> Self {
Self { conditions }
}
pub fn matches(&self, req: &Request<Incoming>) -> bool {
let query = req.uri().query().unwrap_or("");
let query_params: HashMap<String, String> = 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<ipnet::IpNet>,
}
impl RemoteIPMatcher {
pub fn new(ranges: Vec<String>) -> Result<Self> {
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<String, Matcher>,
}
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<Incoming>, 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<Incoming> {
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()));
}
}