1use regex::Regex;
8use std::collections::HashMap;
9use std::sync::Arc;
10use tracing::{debug, trace, warn};
11
12use sentinel_common::types::Priority;
13use sentinel_common::RouteId;
14use sentinel_config::{MatchCondition, RouteConfig, RoutePolicies};
15
16pub struct RouteMatcher {
18 routes: Vec<CompiledRoute>,
20 default_route: Option<RouteId>,
22 cache: Arc<parking_lot::RwLock<RouteCache>>,
24}
25
26struct CompiledRoute {
28 config: Arc<RouteConfig>,
30 id: RouteId,
32 priority: Priority,
34 matchers: Vec<CompiledMatcher>,
36}
37
38enum CompiledMatcher {
40 Path(String),
42 PathPrefix(String),
44 PathRegex(Regex),
46 Host(HostMatcher),
48 Header { name: String, value: Option<String> },
50 Method(Vec<String>),
52 QueryParam { name: String, value: Option<String> },
54}
55
56enum HostMatcher {
58 Exact(String),
60 Wildcard { suffix: String },
62 Regex(Regex),
64}
65
66struct RouteCache {
68 entries: HashMap<String, RouteId>,
70 max_size: usize,
72 access_counter: u64,
74 access_times: HashMap<String, u64>,
76}
77
78impl RouteMatcher {
79 pub fn new(
81 routes: Vec<RouteConfig>,
82 default_route: Option<String>,
83 ) -> Result<Self, RouteError> {
84 let mut compiled_routes = Vec::new();
85
86 for route in routes {
87 let compiled = CompiledRoute::compile(route)?;
88 compiled_routes.push(compiled);
89 }
90
91 compiled_routes.sort_by(|a, b| {
93 b.priority
94 .cmp(&a.priority)
95 .then_with(|| b.specificity().cmp(&a.specificity()))
96 });
97
98 Ok(Self {
99 routes: compiled_routes,
100 default_route: default_route.map(RouteId::new),
101 cache: Arc::new(parking_lot::RwLock::new(RouteCache::new(1000))),
102 })
103 }
104
105 pub fn match_request(&self, req: &RequestInfo) -> Option<RouteMatch> {
107 let cache_key = req.cache_key();
109 if let Some(route_id) = self.cache.write().get(&cache_key) {
110 debug!(route_id = %route_id, "Cache hit for route");
111 if let Some(route) = self.find_route_by_id(&route_id) {
112 return Some(RouteMatch {
113 route_id,
114 config: route.config.clone(),
115 policies: route.config.policies.clone(),
116 });
117 }
118 }
119
120 for route in &self.routes {
122 if route.matches(req) {
123 debug!(
124 route_id = %route.id,
125 priority = ?route.priority,
126 "Route matched"
127 );
128
129 self.cache
131 .write()
132 .insert(cache_key.clone(), route.id.clone());
133
134 return Some(RouteMatch {
135 route_id: route.id.clone(),
136 config: route.config.clone(),
137 policies: route.config.policies.clone(),
138 });
139 }
140 }
141
142 if let Some(ref default_id) = self.default_route {
144 debug!(route_id = %default_id, "Using default route");
145 if let Some(route) = self.find_route_by_id(default_id) {
146 return Some(RouteMatch {
147 route_id: default_id.clone(),
148 config: route.config.clone(),
149 policies: route.config.policies.clone(),
150 });
151 }
152 }
153
154 debug!("No route matched");
155 None
156 }
157
158 fn find_route_by_id(&self, id: &RouteId) -> Option<&CompiledRoute> {
160 self.routes.iter().find(|r| r.id == *id)
161 }
162
163 pub fn clear_cache(&self) {
165 self.cache.write().clear();
166 }
167
168 pub fn cache_stats(&self) -> CacheStats {
170 let cache = self.cache.read();
171 CacheStats {
172 entries: cache.entries.len(),
173 max_size: cache.max_size,
174 hit_rate: 0.0, }
176 }
177}
178
179impl CompiledRoute {
180 fn compile(config: RouteConfig) -> Result<Self, RouteError> {
182 let mut matchers = Vec::new();
183
184 for condition in &config.matches {
185 let compiled = match condition {
186 MatchCondition::Path(path) => CompiledMatcher::Path(path.clone()),
187 MatchCondition::PathPrefix(prefix) => CompiledMatcher::PathPrefix(prefix.clone()),
188 MatchCondition::PathRegex(pattern) => {
189 let regex = Regex::new(pattern).map_err(|e| RouteError::InvalidRegex {
190 pattern: pattern.clone(),
191 error: e.to_string(),
192 })?;
193 CompiledMatcher::PathRegex(regex)
194 }
195 MatchCondition::Host(host) => CompiledMatcher::Host(HostMatcher::parse(host)),
196 MatchCondition::Header { name, value } => CompiledMatcher::Header {
197 name: name.to_lowercase(),
198 value: value.clone(),
199 },
200 MatchCondition::Method(methods) => {
201 CompiledMatcher::Method(methods.iter().map(|m| m.to_uppercase()).collect())
202 }
203 MatchCondition::QueryParam { name, value } => CompiledMatcher::QueryParam {
204 name: name.clone(),
205 value: value.clone(),
206 },
207 };
208 matchers.push(compiled);
209 }
210
211 Ok(Self {
212 id: RouteId::new(&config.id),
213 priority: config.priority,
214 config: Arc::new(config),
215 matchers,
216 })
217 }
218
219 fn matches(&self, req: &RequestInfo) -> bool {
221 for matcher in &self.matchers {
223 if !matcher.matches(req) {
224 trace!(
225 route_id = %self.id,
226 matcher = ?matcher,
227 "Matcher failed"
228 );
229 return false;
230 }
231 }
232 true
233 }
234
235 fn specificity(&self) -> u32 {
237 let mut score = 0;
238 for matcher in &self.matchers {
239 score += match matcher {
240 CompiledMatcher::Path(_) => 1000, CompiledMatcher::PathRegex(_) => 500, CompiledMatcher::PathPrefix(_) => 100, CompiledMatcher::Host(_) => 50,
244 CompiledMatcher::Header { value, .. } => {
245 if value.is_some() {
246 30
247 } else {
248 20
249 }
250 }
251 CompiledMatcher::Method(_) => 10,
252 CompiledMatcher::QueryParam { value, .. } => {
253 if value.is_some() {
254 25
255 } else {
256 15
257 }
258 }
259 };
260 }
261 score
262 }
263}
264
265impl CompiledMatcher {
266 fn matches(&self, req: &RequestInfo) -> bool {
268 match self {
269 Self::Path(path) => req.path == *path,
270 Self::PathPrefix(prefix) => req.path.starts_with(prefix),
271 Self::PathRegex(regex) => regex.is_match(&req.path),
272 Self::Host(host_matcher) => host_matcher.matches(&req.host),
273 Self::Header { name, value } => {
274 if let Some(header_value) = req.headers.get(name) {
275 value.as_ref().map_or(true, |v| header_value == v)
276 } else {
277 false
278 }
279 }
280 Self::Method(methods) => methods.contains(&req.method),
281 Self::QueryParam { name, value } => {
282 if let Some(param_value) = req.query_params.get(name) {
283 value.as_ref().map_or(true, |v| param_value == v)
284 } else {
285 false
286 }
287 }
288 }
289 }
290}
291
292impl HostMatcher {
293 fn parse(pattern: &str) -> Self {
295 if pattern.starts_with("*.") {
296 Self::Wildcard {
298 suffix: pattern[2..].to_string(),
299 }
300 } else if pattern.contains('*') || pattern.contains('[') {
301 if let Ok(regex) = Regex::new(pattern) {
303 Self::Regex(regex)
304 } else {
305 warn!("Invalid host regex pattern: {}, using exact match", pattern);
307 Self::Exact(pattern.to_string())
308 }
309 } else {
310 Self::Exact(pattern.to_string())
312 }
313 }
314
315 fn matches(&self, host: &str) -> bool {
317 match self {
318 Self::Exact(pattern) => host == pattern,
319 Self::Wildcard { suffix } => {
320 host.ends_with(suffix)
321 && host.len() > suffix.len()
322 && host[..host.len() - suffix.len()].ends_with('.')
323 }
324 Self::Regex(regex) => regex.is_match(host),
325 }
326 }
327}
328
329impl RouteCache {
330 fn new(max_size: usize) -> Self {
332 Self {
333 entries: HashMap::new(),
334 max_size,
335 access_counter: 0,
336 access_times: HashMap::new(),
337 }
338 }
339
340 fn get(&mut self, key: &str) -> Option<RouteId> {
342 if let Some(route_id) = self.entries.get(key) {
343 self.access_counter += 1;
344 self.access_times
345 .insert(key.to_string(), self.access_counter);
346 Some(route_id.clone())
347 } else {
348 None
349 }
350 }
351
352 fn insert(&mut self, key: String, route_id: RouteId) {
354 if self.entries.len() >= self.max_size {
356 self.evict_lru();
357 }
358
359 self.access_counter += 1;
360 self.access_times.insert(key.clone(), self.access_counter);
361 self.entries.insert(key, route_id);
362 }
363
364 fn evict_lru(&mut self) {
366 if let Some((key, _)) = self
367 .access_times
368 .iter()
369 .min_by_key(|(_, &time)| time)
370 .map(|(k, v)| (k.clone(), *v))
371 {
372 self.entries.remove(&key);
373 self.access_times.remove(&key);
374 }
375 }
376
377 fn clear(&mut self) {
379 self.entries.clear();
380 self.access_times.clear();
381 self.access_counter = 0;
382 }
383}
384
385#[derive(Debug, Clone)]
387pub struct RequestInfo {
388 pub method: String,
389 pub path: String,
390 pub host: String,
391 pub headers: HashMap<String, String>,
392 pub query_params: HashMap<String, String>,
393}
394
395impl RequestInfo {
396 fn cache_key(&self) -> String {
398 format!("{}:{}:{}", self.method, self.host, self.path)
399 }
400
401 pub fn parse_query_params(path: &str) -> HashMap<String, String> {
403 let mut params = HashMap::new();
404 if let Some(query_start) = path.find('?') {
405 let query = &path[query_start + 1..];
406 for pair in query.split('&') {
407 if let Some(eq_pos) = pair.find('=') {
408 let key = &pair[..eq_pos];
409 let value = &pair[eq_pos + 1..];
410 params.insert(
411 urlencoding::decode(key)
412 .unwrap_or_else(|_| key.into())
413 .into_owned(),
414 urlencoding::decode(value)
415 .unwrap_or_else(|_| value.into())
416 .into_owned(),
417 );
418 } else {
419 params.insert(
420 urlencoding::decode(pair)
421 .unwrap_or_else(|_| pair.into())
422 .into_owned(),
423 String::new(),
424 );
425 }
426 }
427 }
428 params
429 }
430}
431
432#[derive(Debug, Clone)]
434pub struct RouteMatch {
435 pub route_id: RouteId,
436 pub config: Arc<RouteConfig>,
437 pub policies: RoutePolicies,
438}
439
440#[derive(Debug, Clone)]
442pub struct CacheStats {
443 pub entries: usize,
444 pub max_size: usize,
445 pub hit_rate: f64,
446}
447
448#[derive(Debug, thiserror::Error)]
450pub enum RouteError {
451 #[error("Invalid regex pattern '{pattern}': {error}")]
452 InvalidRegex { pattern: String, error: String },
453
454 #[error("Invalid route configuration: {0}")]
455 InvalidConfig(String),
456
457 #[error("Duplicate route ID: {0}")]
458 DuplicateRouteId(String),
459}
460
461impl std::fmt::Debug for CompiledMatcher {
462 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
463 match self {
464 Self::Path(p) => write!(f, "Path({})", p),
465 Self::PathPrefix(p) => write!(f, "PathPrefix({})", p),
466 Self::PathRegex(_) => write!(f, "PathRegex(...)"),
467 Self::Host(_) => write!(f, "Host(...)"),
468 Self::Header { name, .. } => write!(f, "Header({})", name),
469 Self::Method(m) => write!(f, "Method({:?})", m),
470 Self::QueryParam { name, .. } => write!(f, "QueryParam({})", name),
471 }
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use sentinel_common::types::Priority;
479 use sentinel_config::{MatchCondition, RouteConfig};
480
481 fn create_test_route(id: &str, matches: Vec<MatchCondition>) -> RouteConfig {
482 RouteConfig {
483 id: id.to_string(),
484 priority: Priority::Normal,
485 matches,
486 upstream: Some("test_upstream".to_string()),
487 service_type: sentinel_config::ServiceType::Web,
488 policies: Default::default(),
489 filters: vec![],
490 builtin_handler: None,
491 waf_enabled: false,
492 circuit_breaker: None,
493 retry_policy: None,
494 static_files: None,
495 api_schema: None,
496 error_pages: None,
497 }
498 }
499
500 #[test]
501 fn test_path_matching() {
502 let routes = vec![
503 create_test_route(
504 "exact",
505 vec![MatchCondition::Path("/api/v1/users".to_string())],
506 ),
507 create_test_route(
508 "prefix",
509 vec![MatchCondition::PathPrefix("/api/".to_string())],
510 ),
511 ];
512
513 let matcher = RouteMatcher::new(routes, None).unwrap();
514
515 let req = RequestInfo {
516 method: "GET".to_string(),
517 path: "/api/v1/users".to_string(),
518 host: "example.com".to_string(),
519 headers: HashMap::new(),
520 query_params: HashMap::new(),
521 };
522
523 let result = matcher.match_request(&req).unwrap();
524 assert_eq!(result.route_id.as_str(), "exact");
525 }
526
527 #[test]
528 fn test_host_wildcard_matching() {
529 let routes = vec![create_test_route(
530 "wildcard",
531 vec![MatchCondition::Host("*.example.com".to_string())],
532 )];
533
534 let matcher = RouteMatcher::new(routes, None).unwrap();
535
536 let req = RequestInfo {
537 method: "GET".to_string(),
538 path: "/".to_string(),
539 host: "api.example.com".to_string(),
540 headers: HashMap::new(),
541 query_params: HashMap::new(),
542 };
543
544 let result = matcher.match_request(&req).unwrap();
545 assert_eq!(result.route_id.as_str(), "wildcard");
546 }
547
548 #[test]
549 fn test_priority_ordering() {
550 let mut route1 =
551 create_test_route("low", vec![MatchCondition::PathPrefix("/".to_string())]);
552 route1.priority = Priority::Low;
553
554 let mut route2 =
555 create_test_route("high", vec![MatchCondition::PathPrefix("/".to_string())]);
556 route2.priority = Priority::High;
557
558 let routes = vec![route1, route2];
559 let matcher = RouteMatcher::new(routes, None).unwrap();
560
561 let req = RequestInfo {
562 method: "GET".to_string(),
563 path: "/test".to_string(),
564 host: "example.com".to_string(),
565 headers: HashMap::new(),
566 query_params: HashMap::new(),
567 };
568
569 let result = matcher.match_request(&req).unwrap();
570 assert_eq!(result.route_id.as_str(), "high");
571 }
572
573 #[test]
574 fn test_query_param_parsing() {
575 let params = RequestInfo::parse_query_params("/path?foo=bar&baz=qux&empty=");
576 assert_eq!(params.get("foo"), Some(&"bar".to_string()));
577 assert_eq!(params.get("baz"), Some(&"qux".to_string()));
578 assert_eq!(params.get("empty"), Some(&"".to_string()));
579 }
580}