
- 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.
498 lines
16 KiB
Rust
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()));
|
|
}
|
|
} |