1pub mod pattern_trie;
2
3use std::{
4 fmt::{self, Debug, Write},
5 rc::Rc,
6 str::from_utf8,
7 time::Instant,
8};
9
10use regex::bytes::Regex;
11use sozu_command::{
12 logging::CachedTags,
13 proto::command::{
14 HeaderPosition, HstsConfig, PathRule as CommandPathRule, PathRuleKind, RedirectPolicy,
15 RedirectScheme, RulePosition,
16 },
17 response::HttpFrontend,
18 state::ClusterId,
19};
20
21use crate::metrics::names;
22use crate::{
23 protocol::{http::editor::HeaderEditMode, http::parser::Method},
24 router::pattern_trie::{InsertResult, TrieMatches, TrieNode, TrieSubMatch},
25 sozu_command::logging::ansi_palette,
26};
27
28macro_rules! log_module_context {
35 () => {{
36 let (open, reset, _, _, _) = ansi_palette();
37 format!("{open}ROUTER{reset}\t >>>", open = open, reset = reset)
38 }};
39}
40
41pub const MAX_HOSTNAME_LENGTH: usize = 4096;
55
56#[derive(thiserror::Error, PartialEq)]
57pub enum RouterError {
58 #[error("Could not parse rule from frontend path, path_bytes={}", .0.len())]
59 InvalidPathRule(String),
60 #[error("parsing hostname failed, hostname_bytes={}", .hostname.len())]
61 InvalidDomain { hostname: String },
62 #[error("Could not parse host rewrite, rewrite_host_bytes={}", .0.len())]
63 InvalidHostRewrite(String),
64 #[error("Could not parse path rewrite, rewrite_path_bytes={}", .0.len())]
65 InvalidPathRewrite(String),
66 #[error("Could not add route, route_bytes={}", .0.len())]
67 AddRoute(String),
68 #[error("Could not remove route, route_bytes={}", .0.len())]
69 RemoveRoute(String),
70 #[error("route_not_found method={method:?} host_bytes={} path_bytes={}", .host.len(), .path.len())]
71 RouteNotFound {
72 host: String,
73 path: String,
74 method: Method,
75 },
76}
77
78impl fmt::Debug for RouterError {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 fmt::Display::fmt(self, f)
81 }
82}
83
84pub struct Router {
85 pre: Vec<(DomainRule, PathRule, MethodRule, Route)>,
86 pub tree: TrieNode<Vec<(PathRule, MethodRule, Route)>>,
87 post: Vec<(DomainRule, PathRule, MethodRule, Route)>,
88}
89
90impl Default for Router {
91 fn default() -> Self {
92 Self::new()
93 }
94}
95
96impl Router {
97 pub fn new() -> Router {
98 Router {
99 pre: Vec::new(),
100 tree: TrieNode::root(),
101 post: Vec::new(),
102 }
103 }
104
105 pub fn lookup(
114 &self,
115 hostname: &str,
116 path: &str,
117 method: &Method,
118 ) -> Result<RouteResult, RouterError> {
119 let hostname_b = hostname.as_bytes();
120 let path_b = path.as_bytes();
121 for (domain_rule, path_rule, method_rule, route) in &self.pre {
122 if domain_rule.matches(hostname_b)
123 && path_rule.matches(path_b) != PathRuleResult::None
124 && method_rule.matches(method) != MethodRuleResult::None
125 {
126 return Ok(RouteResult::new_no_trie(
127 hostname_b,
128 domain_rule,
129 path_b,
130 path_rule,
131 route,
132 ));
133 }
134 }
135
136 let trie_path: TrieMatches<'_, '_> = Vec::with_capacity(16);
137 if let Some(((_, path_rules), trie_matches)) =
138 self.tree.lookup_with_path(hostname_b, true, trie_path)
139 {
140 let mut prefix_length = 0;
141 let mut matched: Option<(&PathRule, &Route)> = None;
142
143 for (rule, method_rule, route) in path_rules {
144 match rule.matches(path_b) {
145 PathRuleResult::Regex | PathRuleResult::Equals => {
146 match method_rule.matches(method) {
147 MethodRuleResult::Equals => {
148 return Ok(RouteResult::new_with_trie(
149 hostname_b,
150 trie_matches,
151 path_b,
152 rule,
153 route,
154 ));
155 }
156 MethodRuleResult::All => {
157 prefix_length = path_b.len();
158 matched = Some((rule, route));
159 }
160 MethodRuleResult::None => {}
161 }
162 }
163 PathRuleResult::Prefix(size) => {
164 if size >= prefix_length {
165 match method_rule.matches(method) {
166 MethodRuleResult::Equals => {
168 debug_assert!(
172 size >= prefix_length,
173 "longest-prefix selection must never shrink the match length",
174 );
175 prefix_length = size;
176 matched = Some((rule, route));
177 }
178 MethodRuleResult::All => {
179 debug_assert!(
180 size >= prefix_length,
181 "longest-prefix selection must never shrink the match length",
182 );
183 prefix_length = size;
184 matched = Some((rule, route));
185 }
186 MethodRuleResult::None => {}
187 }
188 }
189 }
190 PathRuleResult::None => {}
191 }
192 }
193
194 if let Some((path_rule, route)) = matched {
195 return Ok(RouteResult::new_with_trie(
196 hostname_b,
197 trie_matches,
198 path_b,
199 path_rule,
200 route,
201 ));
202 }
203 }
204
205 for (domain_rule, path_rule, method_rule, route) in self.post.iter() {
206 if domain_rule.matches(hostname_b)
207 && path_rule.matches(path_b) != PathRuleResult::None
208 && method_rule.matches(method) != MethodRuleResult::None
209 {
210 return Ok(RouteResult::new_no_trie(
211 hostname_b,
212 domain_rule,
213 path_b,
214 path_rule,
215 route,
216 ));
217 }
218 }
219
220 Err(RouterError::RouteNotFound {
221 host: hostname.to_owned(),
222 path: path.to_owned(),
223 method: method.to_owned(),
224 })
225 }
226
227 pub fn add_http_front(&mut self, front: &HttpFrontend) -> Result<(), RouterError> {
234 self.add_http_front_with_hsts_origin(front, HstsOrigin::Explicit)
235 }
236
237 pub fn add_http_front_with_hsts_origin(
244 &mut self,
245 front: &HttpFrontend,
246 hsts_origin: HstsOrigin,
247 ) -> Result<(), RouterError> {
248 if front.hostname.len() > MAX_HOSTNAME_LENGTH {
252 return Err(RouterError::InvalidDomain {
253 hostname: front.hostname.clone(),
254 });
255 }
256
257 let path_rule = PathRule::from_config(front.path.clone())
258 .ok_or(RouterError::InvalidPathRule(front.path.to_string()))?;
259
260 let method_rule = MethodRule::new(front.method.clone());
261
262 let has_policy = front.redirect.is_some()
267 || front.redirect_scheme.is_some()
268 || front.redirect_template.is_some()
269 || front.rewrite_host.is_some()
270 || front.rewrite_path.is_some()
271 || front.rewrite_port.is_some()
272 || front.required_auth.unwrap_or(false)
273 || !front.headers.is_empty()
274 || front.hsts.is_some();
275
276 let domain =
277 front
278 .hostname
279 .parse::<DomainRule>()
280 .map_err(|_| RouterError::InvalidDomain {
281 hostname: front.hostname.clone(),
282 })?;
283
284 let route = if has_policy {
285 let redirect = front
286 .redirect
287 .and_then(|r| RedirectPolicy::try_from(r).ok())
288 .unwrap_or(RedirectPolicy::Forward);
289 let redirect_scheme = front
290 .redirect_scheme
291 .and_then(|s| RedirectScheme::try_from(s).ok())
292 .unwrap_or(RedirectScheme::UseSame);
293 let frontend = Frontend::new(
294 &domain,
295 &path_rule,
296 front,
297 redirect,
298 redirect_scheme,
299 front.redirect_template.clone(),
300 front.rewrite_host.clone(),
301 front.rewrite_path.clone(),
302 front.rewrite_port.and_then(|p| u16::try_from(p).ok()),
303 &front.headers,
304 front.required_auth.unwrap_or(false),
305 hsts_origin,
306 )?;
307 Route::Frontend(Rc::new(frontend))
308 } else {
309 match &front.cluster_id {
310 Some(cluster_id) => Route::ClusterId(cluster_id.clone()),
311 None => Route::Deny,
312 }
313 };
314
315 let success = match front.position {
316 RulePosition::Pre => self.add_pre_rule(&domain, &path_rule, &method_rule, &route),
317 RulePosition::Post => self.add_post_rule(&domain, &path_rule, &method_rule, &route),
318 RulePosition::Tree => {
319 self.add_tree_rule(front.hostname.as_bytes(), &path_rule, &method_rule, &route)
320 }
321 };
322 if !success {
323 return Err(RouterError::AddRoute(format!("{front:?}")));
324 }
325 Ok(())
326 }
327
328 pub fn remove_http_front(&mut self, front: &HttpFrontend) -> Result<(), RouterError> {
329 if front.hostname.len() > MAX_HOSTNAME_LENGTH {
332 return Err(RouterError::InvalidDomain {
333 hostname: front.hostname.clone(),
334 });
335 }
336
337 let path_rule = PathRule::from_config(front.path.clone())
338 .ok_or(RouterError::InvalidPathRule(front.path.to_string()))?;
339
340 let method_rule = MethodRule::new(front.method.clone());
341
342 let remove_success = match front.position {
343 RulePosition::Pre => {
344 let domain = front.hostname.parse::<DomainRule>().map_err(|_| {
345 RouterError::InvalidDomain {
346 hostname: front.hostname.clone(),
347 }
348 })?;
349
350 self.remove_pre_rule(&domain, &path_rule, &method_rule)
351 }
352 RulePosition::Post => {
353 let domain = front.hostname.parse::<DomainRule>().map_err(|_| {
354 RouterError::InvalidDomain {
355 hostname: front.hostname.clone(),
356 }
357 })?;
358
359 self.remove_post_rule(&domain, &path_rule, &method_rule)
360 }
361 RulePosition::Tree => {
362 self.remove_tree_rule(front.hostname.as_bytes(), &path_rule, &method_rule)
363 }
364 };
365 if !remove_success {
366 return Err(RouterError::RemoveRoute(format!("{front:?}")));
367 }
368 Ok(())
369 }
370
371 pub fn add_tree_rule(
372 &mut self,
373 hostname: &[u8],
374 path: &PathRule,
375 method: &MethodRule,
376 cluster: &Route,
377 ) -> bool {
378 let hostname = match from_utf8(hostname) {
379 Err(_) => return false,
380 Ok(h) => h,
381 };
382
383 match ::idna::domain_to_ascii(hostname) {
384 Ok(hostname) => {
385 let mut empty = true;
387 if let Some((_, paths)) = self.tree.domain_lookup_mut(hostname.as_bytes(), false) {
388 empty = false;
389 let before = paths.len();
390 if !paths.iter().any(|(p, m, _)| p == path && m == method) {
391 paths.push((path.to_owned(), method.to_owned(), cluster.to_owned()));
392 debug_assert_eq!(
395 paths.len(),
396 before + 1,
397 "appending a tree rule must grow the leaf's rule list by exactly one",
398 );
399 debug_assert!(
400 paths.iter().any(|(p, m, _)| p == path && m == method),
401 "the freshly appended (path, method) rule must be present after insert",
402 );
403 return true;
404 }
405 }
406
407 if empty {
408 let inserted_host = hostname.clone().into_bytes();
413 let insert_result = self.tree.domain_insert(
414 hostname.into_bytes(),
415 vec![(path.to_owned(), method.to_owned(), cluster.to_owned())],
416 );
417 if insert_result == InsertResult::Failed {
429 error!(
437 "{} the route table rejected a malformed hostname, hostname_bytes={}",
438 log_module_context!(),
439 inserted_host.len(),
440 );
441 return false;
442 }
443 debug_assert!(
452 self.tree
453 .domain_lookup_mut(&inserted_host, false)
454 .is_some_and(|(_, paths)| paths
455 .iter()
456 .any(|(p, m, _)| p == path && m == method)),
457 "a freshly inserted tree domain must resolve to its inserted rule",
458 );
459 return true;
460 }
461
462 false
463 }
464 Err(_) => false,
465 }
466 }
467
468 pub fn remove_tree_rule(
469 &mut self,
470 hostname: &[u8],
471 path: &PathRule,
472 method: &MethodRule,
473 ) -> bool {
475 let hostname = match from_utf8(hostname) {
476 Err(_) => return false,
477 Ok(h) => h,
478 };
479
480 match ::idna::domain_to_ascii(hostname) {
481 Ok(hostname) => {
482 let should_delete = {
483 let paths_opt = self.tree.domain_lookup_mut(hostname.as_bytes(), false);
484
485 if let Some((_, paths)) = paths_opt {
486 paths.retain(|(p, m, _)| p != path || m != method);
487 debug_assert!(
490 !paths.iter().any(|(p, m, _)| p == path && m == method),
491 "remove must evict every matching (path, method) rule from the leaf",
492 );
493 }
494
495 paths_opt
496 .as_ref()
497 .map(|(_, paths)| paths.is_empty())
498 .unwrap_or(false)
499 };
500
501 if should_delete {
502 let removed_host = hostname.clone().into_bytes();
503 self.tree.domain_remove(&hostname.into_bytes());
504 debug_assert!(
512 self.tree.domain_lookup_mut(&removed_host, false).is_none(),
513 "a domain whose last rule was removed must be unreachable",
514 );
515 }
516
517 true
518 }
519 Err(_) => false,
520 }
521 }
522
523 pub fn refresh_inheriting_hsts(&mut self, new_hsts: Option<&HstsConfig>) -> usize {
567 let mut refreshed = 0usize;
568 let new_edit = build_listener_hsts_edit(new_hsts);
576 let new_edit_ref = new_edit.as_ref();
577 let promote_lightweight = new_edit_ref.is_some();
578 let mut visit = |route: &mut Route| match route {
579 Route::Frontend(rc) => {
580 if rc.inherits_listener_hsts {
581 let new_frontend = rebuild_with_listener_hsts(rc, new_edit_ref);
582 *rc = Rc::new(new_frontend);
583 refreshed += 1;
584 }
585 }
586 Route::ClusterId(id) => {
587 if promote_lightweight {
588 let promoted = rebuild_with_listener_hsts(
589 &Frontend::minimal_forward(id.clone()),
590 new_edit_ref,
591 );
592 *route = Route::Frontend(Rc::new(promoted));
593 refreshed += 1;
594 }
595 }
596 Route::Deny => {
597 if promote_lightweight {
598 let promoted =
599 rebuild_with_listener_hsts(&Frontend::minimal_deny(), new_edit_ref);
600 *route = Route::Frontend(Rc::new(promoted));
601 refreshed += 1;
602 }
603 }
604 };
605
606 for (_, _, _, route) in self.pre.iter_mut() {
607 visit(route);
608 }
609 self.tree.for_each_value_mut(&mut |paths| {
610 for (_, _, route) in paths.iter_mut() {
611 visit(route);
612 }
613 });
614 for (_, _, _, route) in self.post.iter_mut() {
615 visit(route);
616 }
617 refreshed
618 }
619
620 pub fn add_pre_rule(
621 &mut self,
622 domain: &DomainRule,
623 path: &PathRule,
624 method: &MethodRule,
625 cluster_id: &Route,
626 ) -> bool {
627 let before = self.pre.len();
628 if !self
629 .pre
630 .iter()
631 .any(|(d, p, m, _)| d == domain && p == path && m == method)
632 {
633 self.pre.push((
634 domain.to_owned(),
635 path.to_owned(),
636 method.to_owned(),
637 cluster_id.to_owned(),
638 ));
639 debug_assert_eq!(
643 self.pre.len(),
644 before + 1,
645 "adding a unique pre-rule must push exactly one entry",
646 );
647 debug_assert!(
648 self.pre
649 .iter()
650 .any(|(d, p, m, _)| d == domain && p == path && m == method),
651 "the freshly added pre-rule must be present",
652 );
653 true
654 } else {
655 debug_assert_eq!(
656 self.pre.len(),
657 before,
658 "a duplicate pre-rule must not change the list length",
659 );
660 false
661 }
662 }
663
664 pub fn add_post_rule(
665 &mut self,
666 domain: &DomainRule,
667 path: &PathRule,
668 method: &MethodRule,
669 cluster_id: &Route,
670 ) -> bool {
671 let before = self.post.len();
672 if !self
673 .post
674 .iter()
675 .any(|(d, p, m, _)| d == domain && p == path && m == method)
676 {
677 self.post.push((
678 domain.to_owned(),
679 path.to_owned(),
680 method.to_owned(),
681 cluster_id.to_owned(),
682 ));
683 debug_assert_eq!(
684 self.post.len(),
685 before + 1,
686 "adding a unique post-rule must push exactly one entry",
687 );
688 debug_assert!(
689 self.post
690 .iter()
691 .any(|(d, p, m, _)| d == domain && p == path && m == method),
692 "the freshly added post-rule must be present",
693 );
694 true
695 } else {
696 debug_assert_eq!(
697 self.post.len(),
698 before,
699 "a duplicate post-rule must not change the list length",
700 );
701 false
702 }
703 }
704
705 pub fn remove_pre_rule(
706 &mut self,
707 domain: &DomainRule,
708 path: &PathRule,
709 method: &MethodRule,
710 ) -> bool {
711 let before = self.pre.len();
712 match self
713 .pre
714 .iter()
715 .position(|(d, p, m, _)| d == domain && p == path && m == method)
716 {
717 None => {
718 debug_assert_eq!(
719 self.pre.len(),
720 before,
721 "a no-op pre-rule removal must not change the list length",
722 );
723 false
724 }
725 Some(index) => {
726 debug_assert!(index < self.pre.len(), "found index must be in bounds");
727 self.pre.remove(index);
728 debug_assert_eq!(
730 self.pre.len() + 1,
731 before,
732 "removing a pre-rule must drop exactly one entry",
733 );
734 debug_assert!(
735 !self
736 .pre
737 .iter()
738 .any(|(d, p, m, _)| d == domain && p == path && m == method),
739 "the removed pre-rule must no longer be present",
740 );
741 true
742 }
743 }
744 }
745
746 pub fn remove_post_rule(
747 &mut self,
748 domain: &DomainRule,
749 path: &PathRule,
750 method: &MethodRule,
751 ) -> bool {
752 let before = self.post.len();
753 match self
754 .post
755 .iter()
756 .position(|(d, p, m, _)| d == domain && p == path && m == method)
757 {
758 None => {
759 debug_assert_eq!(
760 self.post.len(),
761 before,
762 "a no-op post-rule removal must not change the list length",
763 );
764 false
765 }
766 Some(index) => {
767 debug_assert!(index < self.post.len(), "found index must be in bounds");
768 self.post.remove(index);
769 debug_assert_eq!(
770 self.post.len() + 1,
771 before,
772 "removing a post-rule must drop exactly one entry",
773 );
774 debug_assert!(
775 !self
776 .post
777 .iter()
778 .any(|(d, p, m, _)| d == domain && p == path && m == method),
779 "the removed post-rule must no longer be present",
780 );
781 true
782 }
783 }
784 }
785
786 pub fn has_hostname(&self, hostname: &str) -> bool {
791 let hostname_b = hostname.as_bytes();
792
793 for (domain_rule, _, _, _) in &self.pre {
795 if domain_rule.matches(hostname_b) {
796 return true;
797 }
798 }
799
800 if let Ok(ascii_hostname) = ::idna::domain_to_ascii(hostname)
802 && self
803 .tree
804 .domain_lookup(ascii_hostname.as_bytes(), false)
805 .is_some()
806 {
807 return true;
808 }
809
810 for (domain_rule, _, _, _) in &self.post {
812 if domain_rule.matches(hostname_b) {
813 return true;
814 }
815 }
816
817 false
818 }
819}
820
821#[derive(Clone, Debug)]
822pub enum DomainRule {
823 Any,
824 Exact(String),
825 Wildcard(String),
831 Regex(Regex),
832}
833
834fn convert_regex_domain_rule(hostname: &str) -> Option<String> {
835 let mut result = String::from("\\A");
841
842 let s = hostname.as_bytes();
843 let mut index = 0;
844 loop {
845 if index >= s.len() {
851 return None;
852 }
853 if s[index] == b'/' {
854 let mut found = false;
855 for i in index + 1..s.len() {
856 if s[i] == b'/' {
857 match std::str::from_utf8(&s[index + 1..i]) {
858 Ok(r) => result.push_str(r),
859 Err(_) => return None,
860 }
861 index = i + 1;
862 found = true;
863 break;
864 }
865 }
866
867 if !found {
868 return None;
869 }
870 } else {
871 let start = index;
872 for i in start..s.len() + 1 {
873 index = i;
874 if i < s.len() && s[i] == b'.' {
875 match std::str::from_utf8(&s[start..i]) {
876 Ok(r) => result.push_str(r),
877 Err(_) => return None,
878 }
879 break;
880 }
881 }
882 if index == s.len() {
883 match std::str::from_utf8(&s[start..]) {
884 Ok(r) => result.push_str(r),
885 Err(_) => return None,
886 }
887 }
888 }
889
890 if index == s.len() {
891 result.push_str("\\z");
892 return Some(result);
893 } else if s[index] == b'.' {
894 result.push_str("\\.");
895 index += 1;
896 } else {
897 return None;
898 }
899 }
900}
901
902impl DomainRule {
903 pub fn matches(&self, hostname: &[u8]) -> bool {
904 match self {
905 DomainRule::Any => true,
906 DomainRule::Wildcard(s) => {
907 debug_assert_eq!(
911 s.as_bytes().first(),
912 Some(&b'*'),
913 "a Wildcard rule must retain its leading '*'",
914 );
915 let suffix = &s.as_bytes()[1..];
916 let matched = hostname
917 .strip_suffix(suffix)
918 .is_some_and(|prefix| !prefix.is_empty() && !prefix.contains(&b'.'));
919 debug_assert!(
923 !matched || hostname.len() > suffix.len(),
924 "a wildcard match requires a non-empty leftmost label before the suffix",
925 );
926 matched
927 }
928 DomainRule::Exact(s) => s.as_bytes() == hostname,
929 DomainRule::Regex(r) => {
930 let start = Instant::now();
931 let is_a_match = r.is_match(hostname);
932 let now = Instant::now();
933 time!(
934 names::event_loop::REGEX_MATCHING_TIME,
935 (now - start).as_millis()
936 );
937 is_a_match
938 }
939 }
940 }
941}
942
943impl std::cmp::PartialEq for DomainRule {
944 fn eq(&self, other: &Self) -> bool {
945 match (self, other) {
946 (DomainRule::Any, DomainRule::Any) => true,
947 (DomainRule::Wildcard(s1), DomainRule::Wildcard(s2)) => s1 == s2,
948 (DomainRule::Exact(s1), DomainRule::Exact(s2)) => s1 == s2,
949 (DomainRule::Regex(r1), DomainRule::Regex(r2)) => r1.as_str() == r2.as_str(),
950 _ => false,
951 }
952 }
953}
954
955impl std::str::FromStr for DomainRule {
956 type Err = ();
957
958 fn from_str(s: &str) -> Result<Self, Self::Err> {
959 Ok(if s == "*" {
960 DomainRule::Any
961 } else if s.contains('/') {
962 match convert_regex_domain_rule(s) {
963 Some(s) => match regex::bytes::Regex::new(&s) {
964 Ok(r) => DomainRule::Regex(r),
965 Err(_) => return Err(()),
966 },
967 None => return Err(()),
968 }
969 } else if s.contains('*') {
970 if s.starts_with('*') {
971 match ::idna::domain_to_ascii(s) {
972 Ok(r) => DomainRule::Wildcard(r),
973 Err(_) => return Err(()),
974 }
975 } else {
976 return Err(());
977 }
978 } else {
979 match ::idna::domain_to_ascii(s) {
980 Ok(r) => DomainRule::Exact(r),
981 Err(_) => return Err(()),
982 }
983 })
984 }
985}
986
987#[derive(Clone, Debug)]
988pub enum PathRule {
989 Prefix(String),
990 Regex(Regex),
991 Equals(String),
992}
993
994#[derive(PartialEq, Eq)]
995pub enum PathRuleResult {
996 Regex,
997 Prefix(usize),
998 Equals,
999 None,
1000}
1001
1002impl PathRule {
1003 pub fn matches(&self, path: &[u8]) -> PathRuleResult {
1004 match self {
1005 PathRule::Prefix(prefix) => {
1006 if path.starts_with(prefix.as_bytes()) {
1007 debug_assert!(
1011 prefix.len() <= path.len(),
1012 "a matching prefix cannot be longer than the path it matched",
1013 );
1014 PathRuleResult::Prefix(prefix.len())
1015 } else {
1016 PathRuleResult::None
1017 }
1018 }
1019 PathRule::Regex(regex) => {
1020 let start = Instant::now();
1021 let is_a_match = regex.is_match(path);
1022 let now = Instant::now();
1023 time!(
1024 names::event_loop::REGEX_MATCHING_TIME,
1025 (now - start).as_millis()
1026 );
1027
1028 if is_a_match {
1029 PathRuleResult::Regex
1030 } else {
1031 PathRuleResult::None
1032 }
1033 }
1034 PathRule::Equals(pattern) => {
1035 if path == pattern.as_bytes() {
1036 PathRuleResult::Equals
1037 } else {
1038 PathRuleResult::None
1039 }
1040 }
1041 }
1042 }
1043
1044 pub fn from_config(rule: CommandPathRule) -> Option<Self> {
1045 match PathRuleKind::try_from(rule.kind) {
1046 Ok(PathRuleKind::Prefix) => Some(PathRule::Prefix(rule.value)),
1047 Ok(PathRuleKind::Regex) => Regex::new(&rule.value).ok().map(PathRule::Regex),
1048 Ok(PathRuleKind::Equals) => Some(PathRule::Equals(rule.value)),
1049 Err(_) => None,
1050 }
1051 }
1052}
1053
1054impl std::cmp::PartialEq for PathRule {
1055 fn eq(&self, other: &Self) -> bool {
1056 match (self, other) {
1057 (PathRule::Prefix(s1), PathRule::Prefix(s2)) => s1 == s2,
1058 (PathRule::Regex(r1), PathRule::Regex(r2)) => r1.as_str() == r2.as_str(),
1059 _ => false,
1060 }
1061 }
1062}
1063
1064#[derive(Clone, Debug, PartialEq, Eq)]
1065pub struct MethodRule {
1066 pub inner: Option<Method>,
1067}
1068
1069#[derive(PartialEq, Eq)]
1070pub enum MethodRuleResult {
1071 All,
1072 Equals,
1073 None,
1074}
1075
1076impl MethodRule {
1077 pub fn new(method: Option<String>) -> Self {
1078 MethodRule {
1079 inner: method.map(|s| Method::new(s.as_bytes())),
1080 }
1081 }
1082
1083 pub fn matches(&self, method: &Method) -> MethodRuleResult {
1084 match self.inner {
1085 None => MethodRuleResult::All,
1086 Some(ref m) => {
1087 if method == m {
1088 MethodRuleResult::Equals
1089 } else {
1090 MethodRuleResult::None
1091 }
1092 }
1093 }
1094 }
1095}
1096
1097#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1119pub enum Route {
1120 Deny,
1122 ClusterId(ClusterId),
1124 Frontend(Rc<Frontend>),
1129}
1130
1131fn build_listener_hsts_edit(new_hsts: Option<&HstsConfig>) -> Option<HeaderEdit> {
1158 let cfg = new_hsts?;
1159 if !matches!(cfg.enabled, Some(true)) {
1160 return None;
1161 }
1162 let rendered = render_hsts(cfg)?;
1163 let mode = if matches!(cfg.force_replace_backend, Some(true)) {
1164 HeaderEditMode::Set
1165 } else {
1166 HeaderEditMode::SetIfAbsent
1167 };
1168 Some(HeaderEdit {
1169 key: Rc::from(&b"strict-transport-security"[..]),
1170 val: rendered.into_bytes().into(),
1171 mode,
1172 })
1173}
1174
1175fn rebuild_with_listener_hsts(frontend: &Frontend, new_edit: Option<&HeaderEdit>) -> Frontend {
1191 let mut headers_response: Vec<HeaderEdit> = frontend
1193 .headers_response
1194 .iter()
1195 .filter(|edit| !edit.key.eq_ignore_ascii_case(b"strict-transport-security"))
1196 .cloned()
1197 .collect();
1198
1199 if let Some(edit) = new_edit {
1202 headers_response.push(edit.clone());
1203 }
1204
1205 Frontend {
1206 headers_response: headers_response.into(),
1207 ..frontend.clone()
1209 }
1210}
1211
1212pub fn render_hsts(cfg: &HstsConfig) -> Option<String> {
1225 let max_age = cfg.max_age?;
1226 let mut s = format!("max-age={max_age}");
1227 if matches!(cfg.include_subdomains, Some(true)) {
1228 s.push_str("; includeSubDomains");
1229 }
1230 if matches!(cfg.preload, Some(true)) {
1231 s.push_str("; preload");
1232 }
1233 Some(s)
1234}
1235
1236#[derive(Clone, PartialEq, Eq)]
1248pub struct HeaderEdit {
1249 pub key: Rc<[u8]>,
1250 pub val: Rc<[u8]>,
1251 pub mode: HeaderEditMode,
1252}
1253
1254impl Debug for HeaderEdit {
1255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1256 f.write_fmt(format_args!(
1257 "({:?}, {:?}, {:?})",
1258 String::from_utf8_lossy(&self.key),
1259 String::from_utf8_lossy(&self.val),
1260 self.mode,
1261 ))
1262 }
1263}
1264
1265#[derive(Debug, Clone, PartialEq, Eq)]
1273enum RewritePart {
1274 String(String),
1275 Host(usize),
1276 Path(usize),
1277}
1278
1279#[derive(Debug, Clone, PartialEq, Eq)]
1294pub struct RewriteParts(Vec<RewritePart>);
1295
1296impl RewriteParts {
1297 pub fn parse(
1309 template: &str,
1310 host_cap_cap: usize,
1311 path_cap_cap: usize,
1312 used_index_host: &mut usize,
1313 used_index_path: &mut usize,
1314 ) -> Option<Self> {
1315 let mut result = Vec::new();
1316 let mut i = 0;
1317 let pattern = template.as_bytes();
1318 while i < pattern.len() {
1319 if pattern[i] == b'$' {
1320 let is_host = if pattern[i..].starts_with(b"$HOST[") {
1321 i += 6;
1322 true
1323 } else if pattern[i..].starts_with(b"$PATH[") {
1324 i += 6;
1325 false
1326 } else {
1327 return None;
1328 };
1329 let mut index = 0usize;
1330 let digits_start = i;
1331 while i < pattern.len() && pattern[i].is_ascii_digit() {
1332 index = index
1333 .checked_mul(10)?
1334 .checked_add((pattern[i] - b'0') as usize)?;
1335 i += 1;
1336 }
1337 if i == digits_start {
1338 return None;
1340 }
1341 if i >= pattern.len() || pattern[i] != b']' {
1342 return None;
1343 }
1344 if is_host {
1345 if index >= host_cap_cap {
1346 return None;
1347 }
1348 if index >= *used_index_host {
1349 *used_index_host = index + 1;
1350 }
1351 result.push(RewritePart::Host(index));
1352 } else {
1353 if index >= path_cap_cap {
1354 return None;
1355 }
1356 if index >= *used_index_path {
1357 *used_index_path = index + 1;
1358 }
1359 result.push(RewritePart::Path(index));
1360 }
1361 i += 1; } else {
1363 let start = i;
1364 while i < pattern.len() && pattern[i] != b'$' {
1365 i += 1;
1366 }
1367 result.push(RewritePart::String(template[start..i].to_owned()));
1372 }
1373 }
1374 debug_assert!(
1377 result.iter().all(|part| match part {
1378 RewritePart::Host(idx) => *idx < host_cap_cap,
1379 RewritePart::Path(idx) => *idx < path_cap_cap,
1380 RewritePart::String(_) => true,
1381 }),
1382 "a parsed rewrite template must only reference captures within the rule's caps",
1383 );
1384 debug_assert!(
1385 *used_index_host <= host_cap_cap && *used_index_path <= path_cap_cap,
1386 "the highest referenced capture index cannot exceed the cap",
1387 );
1388 Some(Self(result))
1389 }
1390
1391 pub fn run(&self, host_captures: &[&str], path_captures: &[&str]) -> String {
1396 let mut cap = 0usize;
1397 for part in &self.0 {
1398 cap += match part {
1399 RewritePart::String(s) => s.len(),
1400 RewritePart::Host(i) => host_captures.get(*i).map(|s| s.len()).unwrap_or(0),
1401 RewritePart::Path(i) => path_captures.get(*i).map(|s| s.len()).unwrap_or(0),
1402 };
1403 }
1404 let mut result = String::with_capacity(cap);
1405 for part in &self.0 {
1406 let _ = match part {
1408 RewritePart::String(s) => result.write_str(s),
1409 RewritePart::Host(i) => result.write_str(host_captures.get(*i).unwrap_or(&"")),
1410 RewritePart::Path(i) => result.write_str(path_captures.get(*i).unwrap_or(&"")),
1411 };
1412 }
1413 debug_assert_eq!(
1417 result.len(),
1418 cap,
1419 "rewrite output length must equal the pre-computed one-pass capacity",
1420 );
1421 result
1422 }
1423}
1424
1425#[derive(Debug, Clone)]
1438pub struct Frontend {
1439 pub cluster_id: Option<ClusterId>,
1440 pub redirect: RedirectPolicy,
1441 pub redirect_scheme: RedirectScheme,
1442 pub redirect_template: Option<String>,
1443 pub capture_cap_host: usize,
1447 pub capture_cap_path: usize,
1451 pub rewrite_host: Option<RewriteParts>,
1452 pub rewrite_path: Option<RewriteParts>,
1453 pub rewrite_port: Option<u16>,
1454 pub headers_request: Rc<[HeaderEdit]>,
1455 pub headers_response: Rc<[HeaderEdit]>,
1456 pub required_auth: bool,
1457 pub tags: Option<Rc<CachedTags>>,
1458 pub inherits_listener_hsts: bool,
1467}
1468
1469#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1477pub enum HstsOrigin {
1478 Explicit,
1483 InheritedFromListenerDefault,
1488}
1489
1490impl PartialEq for Frontend {
1491 fn eq(&self, other: &Self) -> bool {
1492 self.cluster_id == other.cluster_id
1496 && self.redirect == other.redirect
1497 && self.redirect_scheme == other.redirect_scheme
1498 && self.redirect_template == other.redirect_template
1499 && self.rewrite_host == other.rewrite_host
1500 && self.rewrite_path == other.rewrite_path
1501 && self.rewrite_port == other.rewrite_port
1502 && self.headers_request == other.headers_request
1503 && self.headers_response == other.headers_response
1504 && self.required_auth == other.required_auth
1505 }
1506}
1507
1508impl Eq for Frontend {}
1509
1510impl std::hash::Hash for Frontend {
1511 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1512 self.cluster_id.hash(state);
1513 (self.redirect as i32).hash(state);
1516 (self.redirect_scheme as i32).hash(state);
1517 self.redirect_template.hash(state);
1518 self.required_auth.hash(state);
1519 }
1520}
1521
1522impl PartialOrd for Frontend {
1523 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1524 Some(self.cmp(other))
1525 }
1526}
1527
1528impl Ord for Frontend {
1529 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1530 self.cluster_id
1531 .cmp(&other.cluster_id)
1532 .then_with(|| (self.redirect as i32).cmp(&(other.redirect as i32)))
1533 .then_with(|| (self.redirect_scheme as i32).cmp(&(other.redirect_scheme as i32)))
1534 .then_with(|| self.redirect_template.cmp(&other.redirect_template))
1535 .then_with(|| self.required_auth.cmp(&other.required_auth))
1536 }
1537}
1538
1539impl Frontend {
1540 #[allow(clippy::too_many_arguments)]
1561 pub fn new(
1562 domain_rule: &DomainRule,
1563 path_rule: &PathRule,
1564 front: &HttpFrontend,
1565 redirect: RedirectPolicy,
1566 redirect_scheme: RedirectScheme,
1567 redirect_template: Option<String>,
1568 rewrite_host: Option<String>,
1569 rewrite_path: Option<String>,
1570 rewrite_port: Option<u16>,
1571 headers: &[sozu_command::proto::command::Header],
1572 required_auth: bool,
1573 hsts_origin: HstsOrigin,
1574 ) -> Result<Self, RouterError> {
1575 let hsts = front.hsts.as_ref();
1582 let inherits_listener_hsts =
1583 matches!(hsts_origin, HstsOrigin::InheritedFromListenerDefault) && hsts.is_some();
1584 let cluster_id = front.cluster_id.clone();
1585 let tags = front
1586 .tags
1587 .clone()
1588 .map(|tags| Rc::new(CachedTags::new(tags)));
1589
1590 let redirect_template = redirect_template.filter(|s| !s.is_empty());
1595 let rewrite_host = rewrite_host.filter(|s| !s.is_empty());
1596 let rewrite_path = rewrite_path.filter(|s| !s.is_empty());
1597
1598 let deny = match (&cluster_id, redirect) {
1599 (_, RedirectPolicy::Unauthorized) => true,
1600 (None, RedirectPolicy::Forward) => {
1601 let (domain_kind, domain_bytes) = match &domain_rule {
1602 DomainRule::Any => ("any", 0),
1603 DomainRule::Exact(value) => ("exact", value.len()),
1604 DomainRule::Wildcard(value) => ("wildcard", value.len()),
1605 DomainRule::Regex(value) => ("regex", value.as_str().len()),
1606 };
1607 let (path_kind, path_bytes) = match &path_rule {
1608 PathRule::Prefix(value) => ("prefix", value.len()),
1609 PathRule::Regex(value) => ("regex", value.as_str().len()),
1610 PathRule::Equals(value) => ("equals", value.len()),
1611 };
1612 warn!(
1613 "{} Frontend[domain_kind={}, domain_bytes={}, path_kind={}, path_bytes={}]: forward on clusterless frontends are unauthorized",
1614 log_module_context!(),
1615 domain_kind,
1616 domain_bytes,
1617 path_kind,
1618 path_bytes,
1619 );
1620 true
1621 }
1622 _ => false,
1623 };
1624 if deny {
1625 let mut deny_headers_response: Vec<HeaderEdit> = Vec::new();
1633 if let Some(cfg) = hsts
1634 && matches!(cfg.enabled, Some(true))
1635 && let Some(rendered) = render_hsts(cfg)
1636 {
1637 let mode = if matches!(cfg.force_replace_backend, Some(true)) {
1638 HeaderEditMode::Set
1639 } else {
1640 HeaderEditMode::SetIfAbsent
1641 };
1642 deny_headers_response.push(HeaderEdit {
1643 key: Rc::from(&b"strict-transport-security"[..]),
1644 val: rendered.into_bytes().into(),
1645 mode,
1646 });
1647 crate::incr!(names::http::HSTS_FRONTEND_ADDED);
1648 }
1649
1650 return Ok(Self {
1651 cluster_id,
1652 redirect: RedirectPolicy::Unauthorized,
1653 redirect_scheme,
1654 redirect_template: None,
1655 capture_cap_host: 0,
1656 capture_cap_path: 0,
1657 rewrite_host: None,
1658 rewrite_path: None,
1659 rewrite_port: None,
1660 headers_request: Rc::new([]),
1661 headers_response: deny_headers_response.into(),
1662 required_auth,
1663 tags,
1664 inherits_listener_hsts,
1665 });
1666 }
1667
1668 let mut capture_cap_host = match domain_rule {
1673 DomainRule::Any => 1,
1674 DomainRule::Exact(_) => 1,
1675 DomainRule::Wildcard(_) => 2,
1676 DomainRule::Regex(regex) => regex.captures_len(),
1677 };
1678 let mut capture_cap_path = match path_rule {
1679 PathRule::Equals(_) => 1,
1680 PathRule::Prefix(_) => 2,
1681 PathRule::Regex(regex) => regex.captures_len(),
1682 };
1683 let mut used_capture_host = 0usize;
1684 let mut used_capture_path = 0usize;
1685 let rewrite_host_parts = if let Some(p) = rewrite_host {
1686 Some(
1687 RewriteParts::parse(
1688 &p,
1689 capture_cap_host,
1690 capture_cap_path,
1691 &mut used_capture_host,
1692 &mut used_capture_path,
1693 )
1694 .ok_or(RouterError::InvalidHostRewrite(p))?,
1695 )
1696 } else {
1697 None
1698 };
1699 let rewrite_path_parts = if let Some(p) = rewrite_path {
1700 Some(
1701 RewriteParts::parse(
1702 &p,
1703 capture_cap_host,
1704 capture_cap_path,
1705 &mut used_capture_host,
1706 &mut used_capture_path,
1707 )
1708 .ok_or(RouterError::InvalidPathRewrite(p))?,
1709 )
1710 } else {
1711 None
1712 };
1713 if used_capture_host == 0 {
1716 capture_cap_host = 0;
1717 }
1718 if used_capture_path == 0 {
1719 capture_cap_path = 0;
1720 }
1721
1722 let mut headers_request = Vec::new();
1723 let mut headers_response = Vec::new();
1724 for header in headers {
1725 let edit = HeaderEdit {
1726 key: header.key.as_bytes().into(),
1727 val: header.val.as_bytes().into(),
1728 mode: HeaderEditMode::Append,
1729 };
1730 match header.position() {
1731 HeaderPosition::Request => headers_request.push(edit),
1732 HeaderPosition::Response => headers_response.push(edit),
1733 HeaderPosition::Both => {
1734 headers_request.push(edit.clone());
1735 headers_response.push(edit);
1736 }
1737 HeaderPosition::Unspecified => {
1743 warn!(
1744 "{} dropping {:?} with HEADER_POSITION_UNSPECIFIED",
1745 log_module_context!(),
1746 header,
1747 );
1748 }
1749 }
1750 }
1751
1752 if let Some(cfg) = hsts
1760 && matches!(cfg.enabled, Some(true))
1761 {
1762 if let Some(rendered) = render_hsts(cfg) {
1763 let mode = if matches!(cfg.force_replace_backend, Some(true)) {
1769 HeaderEditMode::Set
1770 } else {
1771 HeaderEditMode::SetIfAbsent
1772 };
1773 headers_response.push(HeaderEdit {
1774 key: Rc::from(&b"strict-transport-security"[..]),
1775 val: rendered.into_bytes().into(),
1776 mode,
1777 });
1778 crate::incr!(names::http::HSTS_FRONTEND_ADDED);
1779 } else {
1780 warn!(
1788 "{} HSTS enabled = true on frontend cluster_id_bytes={:?} but render_hsts \
1789 returned None (max_age missing). Frontend will not emit \
1790 Strict-Transport-Security; the config layer that built \
1791 this HstsConfig must substitute DEFAULT_HSTS_MAX_AGE.",
1792 log_module_context!(),
1793 cluster_id.as_ref().map(String::len),
1794 );
1795 crate::incr!(names::http::HSTS_UNRENDERED);
1796 }
1797 }
1798
1799 Ok(Frontend {
1800 cluster_id,
1801 redirect,
1802 redirect_scheme,
1803 redirect_template,
1804 capture_cap_host,
1805 capture_cap_path,
1806 rewrite_host: rewrite_host_parts,
1807 rewrite_path: rewrite_path_parts,
1808 rewrite_port,
1809 headers_request: headers_request.into(),
1810 headers_response: headers_response.into(),
1811 required_auth,
1812 tags,
1813 inherits_listener_hsts,
1814 })
1815 }
1816
1817 pub(crate) fn minimal_forward(cluster_id: ClusterId) -> Self {
1828 Self {
1829 cluster_id: Some(cluster_id),
1830 redirect: RedirectPolicy::Forward,
1831 redirect_scheme: RedirectScheme::UseSame,
1832 redirect_template: None,
1833 capture_cap_host: 0,
1834 capture_cap_path: 0,
1835 rewrite_host: None,
1836 rewrite_path: None,
1837 rewrite_port: None,
1838 headers_request: Rc::new([]),
1839 headers_response: Rc::new([]),
1840 required_auth: false,
1841 tags: None,
1842 inherits_listener_hsts: true,
1843 }
1844 }
1845
1846 pub(crate) fn minimal_deny() -> Self {
1857 Self {
1858 cluster_id: None,
1859 redirect: RedirectPolicy::Unauthorized,
1860 redirect_scheme: RedirectScheme::UseSame,
1861 redirect_template: None,
1862 capture_cap_host: 0,
1863 capture_cap_path: 0,
1864 rewrite_host: None,
1865 rewrite_path: None,
1866 rewrite_port: None,
1867 headers_request: Rc::new([]),
1868 headers_response: Rc::new([]),
1869 required_auth: false,
1870 tags: None,
1871 inherits_listener_hsts: true,
1872 }
1873 }
1874}
1875
1876#[derive(Debug, Clone, PartialEq)]
1891pub struct RouteResult {
1892 pub cluster_id: Option<ClusterId>,
1893 pub redirect: RedirectPolicy,
1894 pub redirect_scheme: RedirectScheme,
1895 pub redirect_template: Option<String>,
1896 pub rewritten_host: Option<String>,
1897 pub rewritten_path: Option<String>,
1898 pub rewritten_port: Option<u16>,
1899 pub headers_request: Rc<[HeaderEdit]>,
1900 pub headers_response: Rc<[HeaderEdit]>,
1901 pub required_auth: bool,
1902 pub tags: Option<Rc<CachedTags>>,
1903}
1904
1905impl RouteResult {
1906 pub fn deny(cluster_id: Option<ClusterId>) -> Self {
1908 Self {
1909 cluster_id,
1910 redirect: RedirectPolicy::Unauthorized,
1911 redirect_scheme: RedirectScheme::UseSame,
1912 redirect_template: None,
1913 rewritten_host: None,
1914 rewritten_path: None,
1915 rewritten_port: None,
1916 headers_request: Rc::new([]),
1917 headers_response: Rc::new([]),
1918 required_auth: false,
1919 tags: None,
1920 }
1921 }
1922
1923 pub fn forward(cluster_id: ClusterId) -> Self {
1926 Self {
1927 cluster_id: Some(cluster_id),
1928 redirect: RedirectPolicy::Forward,
1929 redirect_scheme: RedirectScheme::UseSame,
1930 redirect_template: None,
1931 rewritten_host: None,
1932 rewritten_path: None,
1933 rewritten_port: None,
1934 headers_request: Rc::new([]),
1935 headers_response: Rc::new([]),
1936 required_auth: false,
1937 tags: None,
1938 }
1939 }
1940
1941 fn from_frontend(
1944 frontend: &Frontend,
1945 captures_host: Vec<&str>,
1946 path: &[u8],
1947 path_rule: &PathRule,
1948 ) -> Self {
1949 if frontend.redirect == RedirectPolicy::Unauthorized {
1957 return Self {
1958 cluster_id: frontend.cluster_id.clone(),
1959 redirect: RedirectPolicy::Unauthorized,
1960 redirect_scheme: frontend.redirect_scheme,
1961 redirect_template: frontend.redirect_template.clone(),
1962 rewritten_host: None,
1963 rewritten_path: None,
1964 rewritten_port: None,
1965 headers_request: Rc::new([]),
1966 headers_response: frontend.headers_response.clone(),
1967 required_auth: frontend.required_auth,
1968 tags: frontend.tags.clone(),
1969 };
1970 }
1971
1972 let mut captures_path: Vec<&str> = Vec::with_capacity(frontend.capture_cap_path);
1973 if frontend.capture_cap_path > 0 {
1974 captures_path.push(from_utf8(path).unwrap_or_default());
1975 match path_rule {
1976 PathRule::Prefix(prefix) => {
1977 let tail_start = prefix.len().min(path.len());
1978 captures_path.push(from_utf8(&path[tail_start..]).unwrap_or_default());
1979 }
1980 PathRule::Regex(regex) => {
1981 if let Some(caps) = regex.captures(path) {
1982 captures_path.extend(caps.iter().skip(1).map(|c| {
1983 c.map(|m| from_utf8(m.as_bytes()).unwrap_or_default())
1984 .unwrap_or("")
1985 }));
1986 }
1987 }
1988 PathRule::Equals(_) => {}
1989 }
1990 }
1991
1992 Self {
1993 cluster_id: frontend.cluster_id.clone(),
1994 redirect: frontend.redirect,
1995 redirect_scheme: frontend.redirect_scheme,
1996 redirect_template: frontend.redirect_template.clone(),
1997 rewritten_host: frontend
1998 .rewrite_host
1999 .as_ref()
2000 .map(|rewrite| rewrite.run(&captures_host, &captures_path)),
2001 rewritten_path: frontend
2002 .rewrite_path
2003 .as_ref()
2004 .map(|rewrite| rewrite.run(&captures_host, &captures_path)),
2005 rewritten_port: frontend.rewrite_port,
2006 headers_request: frontend.headers_request.clone(),
2007 headers_response: frontend.headers_response.clone(),
2008 required_auth: frontend.required_auth,
2009 tags: frontend.tags.clone(),
2010 }
2011 }
2012
2013 fn new_no_trie<'a>(
2018 domain: &'a [u8],
2019 domain_rule: &DomainRule,
2020 path: &'a [u8],
2021 path_rule: &PathRule,
2022 route: &Route,
2023 ) -> Self {
2024 let frontend = match route {
2025 Route::Frontend(f) => f.clone(),
2026 Route::ClusterId(id) => return Self::forward(id.clone()),
2027 Route::Deny => return Self::deny(None),
2028 };
2029 let mut captures_host: Vec<&str> = Vec::with_capacity(frontend.capture_cap_host);
2030 if frontend.capture_cap_host > 0 {
2031 captures_host.push(from_utf8(domain).unwrap_or_default());
2032 match domain_rule {
2033 DomainRule::Wildcard(suffix) => {
2034 let head_end = domain.len().saturating_sub(suffix.len().saturating_sub(1));
2035 captures_host.push(from_utf8(&domain[..head_end]).unwrap_or_default());
2036 }
2037 DomainRule::Regex(regex) => {
2038 if let Some(caps) = regex.captures(domain) {
2039 captures_host.extend(caps.iter().skip(1).map(|c| {
2040 c.map(|m| from_utf8(m.as_bytes()).unwrap_or_default())
2041 .unwrap_or("")
2042 }));
2043 }
2044 }
2045 DomainRule::Any | DomainRule::Exact(_) => {}
2046 }
2047 }
2048 Self::from_frontend(&frontend, captures_host, path, path_rule)
2049 }
2050
2051 fn new_with_trie<'a, 'b>(
2056 domain: &'a [u8],
2057 domain_submatches: TrieMatches<'a, 'b>,
2058 path: &'a [u8],
2059 path_rule: &PathRule,
2060 route: &Route,
2061 ) -> Self {
2062 let frontend = match route {
2063 Route::Frontend(f) => f.clone(),
2064 Route::ClusterId(id) => return Self::forward(id.clone()),
2065 Route::Deny => return Self::deny(None),
2066 };
2067 let mut captures_host: Vec<&str> = Vec::with_capacity(frontend.capture_cap_host);
2068 if frontend.capture_cap_host > 0 {
2069 captures_host.push(from_utf8(domain).unwrap_or_default());
2070 for submatch in &domain_submatches {
2071 match submatch {
2072 TrieSubMatch::Wildcard(part) => {
2073 captures_host.push(from_utf8(part).unwrap_or_default());
2074 }
2075 TrieSubMatch::Regexp(part, regex) => {
2076 if let Some(caps) = regex.captures(part) {
2077 captures_host.extend(caps.iter().skip(1).map(|c| {
2078 c.map(|m| from_utf8(m.as_bytes()).unwrap_or_default())
2079 .unwrap_or("")
2080 }));
2081 }
2082 }
2083 }
2084 }
2085 }
2086 Self::from_frontend(&frontend, captures_host, path, path_rule)
2087 }
2088}
2089
2090#[cfg(test)]
2091mod tests {
2092 use super::*;
2093
2094 fn test_http_frontend() -> HttpFrontend {
2095 HttpFrontend {
2096 cluster_id: Some("cluster".to_owned()),
2097 address: "127.0.0.1:8080"
2098 .parse()
2099 .expect("test frontend address must parse"),
2100 hostname: "example.com".to_owned(),
2101 path: CommandPathRule::prefix("/".to_owned()),
2102 method: None,
2103 position: RulePosition::Tree,
2104 tags: None,
2105 redirect: None,
2106 redirect_scheme: None,
2107 redirect_template: None,
2108 rewrite_host: None,
2109 rewrite_path: None,
2110 rewrite_port: None,
2111 required_auth: None,
2112 headers: Vec::new(),
2113 hsts: None,
2114 }
2115 }
2116
2117 #[test]
2118 fn clusterless_forward_warning_redacts_domain_and_path_rules() {
2119 const DOMAIN_SECRET: &str = "clusterless_domain_secret_sentinel";
2120 const PATH_SECRET: &str = "CLUSTERLESS_PATH_SECRET_SENTINEL";
2121
2122 let domain = format!("{DOMAIN_SECRET}{}", "x".repeat(2048));
2126 let path = format!("/{PATH_SECRET}{}", "x".repeat(4096));
2127 let domain_len = domain.len();
2128 let path_len = path.len();
2129 let output = crate::capture_test_logs(move || {
2130 let mut router = Router::new();
2131 let mut front = test_http_frontend();
2132 front.cluster_id = None;
2133 front.hostname = domain;
2134 front.path = CommandPathRule::prefix(path);
2135 front.redirect = Some(RedirectPolicy::Forward as i32);
2136 router
2137 .add_http_front(&front)
2138 .expect("clusterless forward must be coerced to unauthorized");
2139 });
2140
2141 for secret in [DOMAIN_SECRET, PATH_SECRET] {
2142 assert!(
2143 !output.contains(secret),
2144 "clusterless-forward warning leaked rule marker {secret}"
2145 );
2146 }
2147 for metadata in [
2148 "domain_kind=exact".to_owned(),
2149 format!("domain_bytes={domain_len}"),
2150 "path_kind=prefix".to_owned(),
2151 format!("path_bytes={path_len}"),
2152 ] {
2153 assert!(
2154 output.contains(&metadata),
2155 "clusterless-forward warning omitted bounded metadata {metadata}: {output}"
2156 );
2157 }
2158 assert!(
2159 output.len() <= 512,
2160 "clusterless-forward warning is not bounded: {} bytes",
2161 output.len()
2162 );
2163 }
2164
2165 #[test]
2166 fn malformed_hsts_warning_redacts_cluster_id() {
2167 const CLUSTER_SECRET: &str = "MALFORMED_HSTS_CLUSTER_SECRET_SENTINEL";
2168
2169 let cluster_id = format!("{CLUSTER_SECRET}{}", "x".repeat(4096));
2170 let cluster_id_len = cluster_id.len();
2171 let output = crate::capture_test_logs(move || {
2172 let mut router = Router::new();
2173 let mut front = test_http_frontend();
2174 front.cluster_id = Some(cluster_id);
2175 front.hsts = Some(HstsConfig {
2176 enabled: Some(true),
2177 max_age: None,
2178 include_subdomains: Some(true),
2179 preload: Some(true),
2180 force_replace_backend: Some(false),
2181 });
2182 router
2183 .add_http_front(&front)
2184 .expect("malformed HSTS must preserve routing and omit the header");
2185 });
2186
2187 assert!(
2188 !output.contains(CLUSTER_SECRET),
2189 "malformed-HSTS warning leaked cluster id {CLUSTER_SECRET}"
2190 );
2191 assert!(
2192 output.contains(&format!("cluster_id_bytes=Some({cluster_id_len})")),
2193 "malformed-HSTS warning omitted the bounded cluster id length: {output}"
2194 );
2195 assert!(
2196 output.len() <= 768,
2197 "malformed-HSTS warning is not bounded: {} bytes",
2198 output.len()
2199 );
2200 }
2201
2202 #[test]
2203 fn router_errors_redact_frontend_rule_and_rewrite_fields() {
2204 const PATH_SECRET: &str = "ROUTER_ERROR_PATH_SECRET_SENTINEL";
2205 const HOSTNAME_SECRET: &str = "ROUTER_ERROR_HOSTNAME_SECRET_SENTINEL";
2206 const HOST_REWRITE_SECRET: &str = "ROUTER_ERROR_HOST_REWRITE_SECRET_SENTINEL";
2207 const PATH_REWRITE_SECRET: &str = "ROUTER_ERROR_PATH_REWRITE_SECRET_SENTINEL";
2208
2209 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
2210 let cases = [
2211 (
2212 "path_bytes",
2213 PATH_SECRET,
2214 long_value(PATH_SECRET).len(),
2215 RouterError::InvalidPathRule(long_value(PATH_SECRET)),
2216 ),
2217 (
2218 "hostname_bytes",
2219 HOSTNAME_SECRET,
2220 long_value(HOSTNAME_SECRET).len(),
2221 RouterError::InvalidDomain {
2222 hostname: long_value(HOSTNAME_SECRET),
2223 },
2224 ),
2225 (
2226 "rewrite_host_bytes",
2227 HOST_REWRITE_SECRET,
2228 long_value(HOST_REWRITE_SECRET).len(),
2229 RouterError::InvalidHostRewrite(long_value(HOST_REWRITE_SECRET)),
2230 ),
2231 (
2232 "rewrite_path_bytes",
2233 PATH_REWRITE_SECRET,
2234 long_value(PATH_REWRITE_SECRET).len(),
2235 RouterError::InvalidPathRewrite(long_value(PATH_REWRITE_SECRET)),
2236 ),
2237 ];
2238
2239 for (length_label, secret, value_len, error) in cases {
2240 for (format_label, output) in [
2241 ("Display", error.to_string()),
2242 ("Debug", format!("{error:?}")),
2243 ] {
2244 assert!(
2245 !output.contains(secret),
2246 "RouterError {format_label} leaked frontend marker {secret}"
2247 );
2248 let metadata = format!("{length_label}={value_len}");
2249 assert!(
2250 output.contains(&metadata),
2251 "RouterError {format_label} omitted bounded metadata {metadata}: {output}"
2252 );
2253 assert!(
2254 output.len() <= 256,
2255 "RouterError {format_label} output is not bounded: {} bytes",
2256 output.len()
2257 );
2258 }
2259 }
2260 }
2261
2262 #[test]
2263 fn route_miss_error_retains_inputs_but_bounds_textual_formatting() {
2264 const HOST_SECRET: &str = "ROUTE_MISS_HOST_SECRET_SENTINEL";
2265 const PATH_SECRET: &str = "ROUTE_MISS_PATH_SECRET_SENTINEL";
2266 const METHOD_SECRET: &str = "ROUTE_MISS_METHOD_SECRET_SENTINEL";
2267
2268 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
2269 let host = long_value(HOST_SECRET);
2270 let path = long_value(PATH_SECRET);
2271 let method = Method::Custom(long_value(METHOD_SECRET));
2272 let error = match Router::new().lookup(&host, &path, &method) {
2273 Err(error) => error,
2274 Ok(_) => panic!("empty router must return a route miss"),
2275 };
2276
2277 match &error {
2278 RouterError::RouteNotFound {
2279 host: retained_host,
2280 path: retained_path,
2281 method: retained_method,
2282 } => {
2283 assert_eq!(retained_host, &host);
2284 assert_eq!(retained_path, &path);
2285 assert_eq!(retained_method, &method);
2286 }
2287 other => panic!("expected RouterError::RouteNotFound, got {other:?}"),
2288 }
2289
2290 for output in [error.to_string(), format!("{error:?}")] {
2291 for secret in [HOST_SECRET, PATH_SECRET, METHOD_SECRET] {
2292 assert!(
2293 !output.contains(secret),
2294 "route miss formatting leaked {secret}: {output}"
2295 );
2296 }
2297 for metadata in [
2298 format!("host_bytes={}", host.len()),
2299 format!("path_bytes={}", path.len()),
2300 format!("bytes={}", method.as_ref().len()),
2301 ] {
2302 assert!(
2303 output.contains(&metadata),
2304 "route miss formatting omitted {metadata}: {output}"
2305 );
2306 }
2307 assert!(
2308 output.len() <= 256,
2309 "route miss formatting is not bounded: {} bytes",
2310 output.len()
2311 );
2312 }
2313 }
2314
2315 #[test]
2316 fn render_hsts_max_age_only() {
2317 let cfg = HstsConfig {
2318 enabled: Some(true),
2319 max_age: Some(31_536_000),
2320 include_subdomains: None,
2321 preload: None,
2322 force_replace_backend: None,
2323 };
2324 assert_eq!(render_hsts(&cfg), Some("max-age=31536000".to_owned()));
2325 }
2326
2327 #[test]
2328 fn render_hsts_with_include_subdomains() {
2329 let cfg = HstsConfig {
2330 enabled: Some(true),
2331 max_age: Some(31_536_000),
2332 include_subdomains: Some(true),
2333 preload: None,
2334 force_replace_backend: None,
2335 };
2336 assert_eq!(
2337 render_hsts(&cfg),
2338 Some("max-age=31536000; includeSubDomains".to_owned())
2339 );
2340 }
2341
2342 #[test]
2343 fn render_hsts_with_preload_only() {
2344 let cfg = HstsConfig {
2345 enabled: Some(true),
2346 max_age: Some(63_072_000),
2347 include_subdomains: None,
2348 preload: Some(true),
2349 force_replace_backend: None,
2350 };
2351 assert_eq!(
2352 render_hsts(&cfg),
2353 Some("max-age=63072000; preload".to_owned())
2354 );
2355 }
2356
2357 #[test]
2358 fn render_hsts_full() {
2359 let cfg = HstsConfig {
2360 enabled: Some(true),
2361 max_age: Some(31_536_000),
2362 include_subdomains: Some(true),
2363 preload: Some(true),
2364 force_replace_backend: None,
2365 };
2366 assert_eq!(
2367 render_hsts(&cfg),
2368 Some("max-age=31536000; includeSubDomains; preload".to_owned())
2369 );
2370 }
2371
2372 #[test]
2373 fn render_hsts_kill_switch_max_age_zero() {
2374 let cfg = HstsConfig {
2375 enabled: Some(true),
2376 max_age: Some(0),
2377 include_subdomains: Some(true),
2378 preload: None,
2379 force_replace_backend: None,
2380 };
2381 assert_eq!(
2385 render_hsts(&cfg),
2386 Some("max-age=0; includeSubDomains".to_owned())
2387 );
2388 }
2389
2390 #[test]
2391 fn render_hsts_omitted_when_max_age_missing() {
2392 let cfg = HstsConfig {
2393 enabled: Some(true),
2394 max_age: None,
2395 include_subdomains: Some(true),
2396 preload: None,
2397 force_replace_backend: None,
2398 };
2399 assert_eq!(render_hsts(&cfg), None);
2403 }
2404
2405 #[test]
2406 fn rebuild_with_listener_hsts_replaces_existing_entry() {
2407 let frontend = Frontend {
2411 cluster_id: Some("api".to_owned()),
2412 redirect: RedirectPolicy::Forward,
2413 redirect_scheme: RedirectScheme::UseSame,
2414 redirect_template: None,
2415 capture_cap_host: 0,
2416 capture_cap_path: 0,
2417 rewrite_host: None,
2418 rewrite_path: None,
2419 rewrite_port: None,
2420 headers_request: Rc::new([]),
2421 headers_response: Rc::from(vec![
2422 HeaderEdit {
2423 key: Rc::from(&b"x-cache"[..]),
2424 val: Rc::from(&b"hit"[..]),
2425 mode: HeaderEditMode::Append,
2426 },
2427 HeaderEdit {
2428 key: Rc::from(&b"strict-transport-security"[..]),
2429 val: Rc::from(&b"max-age=31536000"[..]),
2430 mode: HeaderEditMode::SetIfAbsent,
2431 },
2432 ]),
2433 required_auth: false,
2434 tags: None,
2435 inherits_listener_hsts: true,
2436 };
2437 let new_hsts = HstsConfig {
2438 enabled: Some(true),
2439 max_age: Some(63_072_000),
2440 include_subdomains: Some(true),
2441 preload: None,
2442 force_replace_backend: None,
2443 };
2444 let new_edit = build_listener_hsts_edit(Some(&new_hsts));
2445 let rebuilt = rebuild_with_listener_hsts(&frontend, new_edit.as_ref());
2446
2447 let response: Vec<_> = rebuilt.headers_response.iter().collect();
2448 assert_eq!(response.len(), 2, "x-cache + new STS, no leftover STS");
2449 assert_eq!(&*response[0].key, b"x-cache");
2450 assert_eq!(&*response[1].key, b"strict-transport-security");
2451 assert_eq!(
2452 &*response[1].val,
2453 b"max-age=63072000; includeSubDomains".as_slice()
2454 );
2455 assert!(rebuilt.inherits_listener_hsts);
2456 }
2457
2458 #[test]
2459 fn rebuild_with_listener_hsts_strips_when_none() {
2460 let frontend = Frontend {
2463 cluster_id: Some("api".to_owned()),
2464 redirect: RedirectPolicy::Forward,
2465 redirect_scheme: RedirectScheme::UseSame,
2466 redirect_template: None,
2467 capture_cap_host: 0,
2468 capture_cap_path: 0,
2469 rewrite_host: None,
2470 rewrite_path: None,
2471 rewrite_port: None,
2472 headers_request: Rc::new([]),
2473 headers_response: Rc::from(vec![
2474 HeaderEdit {
2475 key: Rc::from(&b"x-cache"[..]),
2476 val: Rc::from(&b"hit"[..]),
2477 mode: HeaderEditMode::Append,
2478 },
2479 HeaderEdit {
2480 key: Rc::from(&b"strict-transport-security"[..]),
2481 val: Rc::from(&b"max-age=31536000"[..]),
2482 mode: HeaderEditMode::SetIfAbsent,
2483 },
2484 ]),
2485 required_auth: false,
2486 tags: None,
2487 inherits_listener_hsts: true,
2488 };
2489 let new_edit = build_listener_hsts_edit(None);
2490 let rebuilt = rebuild_with_listener_hsts(&frontend, new_edit.as_ref());
2491 let response: Vec<_> = rebuilt.headers_response.iter().collect();
2492 assert_eq!(response.len(), 1);
2493 assert_eq!(&*response[0].key, b"x-cache");
2494 }
2495
2496 #[test]
2497 fn rebuild_with_listener_hsts_disabled_strips() {
2498 let frontend = Frontend {
2501 cluster_id: Some("api".to_owned()),
2502 redirect: RedirectPolicy::Forward,
2503 redirect_scheme: RedirectScheme::UseSame,
2504 redirect_template: None,
2505 capture_cap_host: 0,
2506 capture_cap_path: 0,
2507 rewrite_host: None,
2508 rewrite_path: None,
2509 rewrite_port: None,
2510 headers_request: Rc::new([]),
2511 headers_response: Rc::from(vec![HeaderEdit {
2512 key: Rc::from(&b"strict-transport-security"[..]),
2513 val: Rc::from(&b"max-age=31536000"[..]),
2514 mode: HeaderEditMode::SetIfAbsent,
2515 }]),
2516 required_auth: false,
2517 tags: None,
2518 inherits_listener_hsts: true,
2519 };
2520 let new_hsts = HstsConfig {
2521 enabled: Some(false),
2522 max_age: None,
2523 include_subdomains: None,
2524 preload: None,
2525 force_replace_backend: None,
2526 };
2527 let new_edit = build_listener_hsts_edit(Some(&new_hsts));
2528 let rebuilt = rebuild_with_listener_hsts(&frontend, new_edit.as_ref());
2529 assert_eq!(rebuilt.headers_response.len(), 0);
2530 }
2531
2532 #[test]
2533 fn refresh_inheriting_hsts_skips_explicit_overrides() {
2534 use crate::router::pattern_trie::TrieNode;
2538 let mut router = Router {
2539 pre: Vec::new(),
2540 tree: TrieNode::root(),
2541 post: Vec::new(),
2542 };
2543 let inheriting = Frontend {
2544 cluster_id: Some("api".to_owned()),
2545 redirect: RedirectPolicy::Forward,
2546 redirect_scheme: RedirectScheme::UseSame,
2547 redirect_template: None,
2548 capture_cap_host: 0,
2549 capture_cap_path: 0,
2550 rewrite_host: None,
2551 rewrite_path: None,
2552 rewrite_port: None,
2553 headers_request: Rc::new([]),
2554 headers_response: Rc::from(vec![HeaderEdit {
2555 key: Rc::from(&b"strict-transport-security"[..]),
2556 val: Rc::from(&b"max-age=31536000"[..]),
2557 mode: HeaderEditMode::SetIfAbsent,
2558 }]),
2559 required_auth: false,
2560 tags: None,
2561 inherits_listener_hsts: true,
2562 };
2563 let explicit = Frontend {
2564 cluster_id: Some("legacy".to_owned()),
2565 redirect: RedirectPolicy::Forward,
2566 redirect_scheme: RedirectScheme::UseSame,
2567 redirect_template: None,
2568 capture_cap_host: 0,
2569 capture_cap_path: 0,
2570 rewrite_host: None,
2571 rewrite_path: None,
2572 rewrite_port: None,
2573 headers_request: Rc::new([]),
2574 headers_response: Rc::from(vec![HeaderEdit {
2575 key: Rc::from(&b"strict-transport-security"[..]),
2576 val: Rc::from(&b"max-age=300"[..]),
2577 mode: HeaderEditMode::SetIfAbsent,
2578 }]),
2579 required_auth: false,
2580 tags: None,
2581 inherits_listener_hsts: false,
2582 };
2583 router.pre.push((
2584 DomainRule::Any,
2585 PathRule::Prefix("/api".to_owned()),
2586 MethodRule::new(None),
2587 Route::Frontend(Rc::new(inheriting)),
2588 ));
2589 router.post.push((
2590 DomainRule::Any,
2591 PathRule::Prefix("/legacy".to_owned()),
2592 MethodRule::new(None),
2593 Route::Frontend(Rc::new(explicit)),
2594 ));
2595
2596 let new_hsts = HstsConfig {
2597 enabled: Some(true),
2598 max_age: Some(63_072_000),
2599 include_subdomains: Some(true),
2600 preload: None,
2601 force_replace_backend: None,
2602 };
2603 let count = router.refresh_inheriting_hsts(Some(&new_hsts));
2604 assert_eq!(count, 1, "only the inheriting frontend should refresh");
2605
2606 if let Route::Frontend(rc) = &router.pre[0].3 {
2607 let response: Vec<_> = rc.headers_response.iter().collect();
2608 assert_eq!(
2609 &*response.last().unwrap().val,
2610 b"max-age=63072000; includeSubDomains".as_slice(),
2611 "inheriting frontend's STS must reflect the new listener default"
2612 );
2613 } else {
2614 panic!("pre[0] should be Route::Frontend");
2615 }
2616 if let Route::Frontend(rc) = &router.post[0].3 {
2617 let response: Vec<_> = rc.headers_response.iter().collect();
2618 assert_eq!(
2619 &*response.last().unwrap().val,
2620 b"max-age=300".as_slice(),
2621 "explicit override must be preserved unchanged"
2622 );
2623 } else {
2624 panic!("post[0] should be Route::Frontend");
2625 }
2626 }
2627
2628 #[test]
2629 fn refresh_inheriting_hsts_promotes_clusterid_on_enable() {
2630 use crate::router::pattern_trie::TrieNode;
2640 let mut router = Router {
2641 pre: Vec::new(),
2642 tree: TrieNode::root(),
2643 post: vec![(
2644 DomainRule::Any,
2645 PathRule::Prefix("/".to_owned()),
2646 MethodRule::new(None),
2647 Route::ClusterId("api".to_owned()),
2648 )],
2649 };
2650
2651 let new_hsts = HstsConfig {
2652 enabled: Some(true),
2653 max_age: Some(31_536_000),
2654 include_subdomains: Some(true),
2655 preload: None,
2656 force_replace_backend: None,
2657 };
2658 let count = router.refresh_inheriting_hsts(Some(&new_hsts));
2659 assert_eq!(count, 1, "the ClusterId entry must be promoted + counted");
2660
2661 let Route::Frontend(rc) = &router.post[0].3 else {
2662 panic!("post[0] should now be Route::Frontend, not the original Route::ClusterId");
2663 };
2664 assert_eq!(rc.cluster_id.as_deref(), Some("api"));
2665 assert_eq!(
2666 rc.redirect,
2667 RedirectPolicy::Forward,
2668 "promoted entry must keep Forward semantics so lookup yields the same backend"
2669 );
2670 assert!(
2671 rc.inherits_listener_hsts,
2672 "promoted entry must mark itself inheriting so the next patch refreshes it"
2673 );
2674 let response: Vec<_> = rc.headers_response.iter().collect();
2675 assert_eq!(
2676 response.len(),
2677 1,
2678 "promoted entry carries exactly one STS edit, no operator headers"
2679 );
2680 assert_eq!(&*response[0].key, b"strict-transport-security");
2681 assert_eq!(
2682 &*response[0].val,
2683 b"max-age=31536000; includeSubDomains".as_slice()
2684 );
2685 }
2686
2687 #[test]
2688 fn refresh_inheriting_hsts_promotes_deny_on_enable() {
2689 use crate::router::pattern_trie::TrieNode;
2695 let mut router = Router {
2696 pre: Vec::new(),
2697 tree: TrieNode::root(),
2698 post: vec![(
2699 DomainRule::Any,
2700 PathRule::Prefix("/forbidden".to_owned()),
2701 MethodRule::new(None),
2702 Route::Deny,
2703 )],
2704 };
2705
2706 let new_hsts = HstsConfig {
2707 enabled: Some(true),
2708 max_age: Some(31_536_000),
2709 include_subdomains: None,
2710 preload: None,
2711 force_replace_backend: None,
2712 };
2713 let count = router.refresh_inheriting_hsts(Some(&new_hsts));
2714 assert_eq!(count, 1);
2715
2716 let Route::Frontend(rc) = &router.post[0].3 else {
2717 panic!("post[0] should now be Route::Frontend, not the original Route::Deny");
2718 };
2719 assert_eq!(rc.cluster_id, None, "promoted Deny stays clusterless");
2720 assert_eq!(
2721 rc.redirect,
2722 RedirectPolicy::Unauthorized,
2723 "promoted Deny must keep Unauthorized so lookup yields a 401"
2724 );
2725 assert!(rc.inherits_listener_hsts);
2726 let response: Vec<_> = rc.headers_response.iter().collect();
2727 assert_eq!(response.len(), 1);
2728 assert_eq!(&*response[0].key, b"strict-transport-security");
2729 assert_eq!(&*response[0].val, b"max-age=31536000".as_slice());
2730 }
2731
2732 #[test]
2733 fn refresh_inheriting_hsts_skips_lightweight_on_disable() {
2734 use crate::router::pattern_trie::TrieNode;
2744 let make_router = || Router {
2745 pre: vec![(
2746 DomainRule::Any,
2747 PathRule::Prefix("/".to_owned()),
2748 MethodRule::new(None),
2749 Route::ClusterId("api".to_owned()),
2750 )],
2751 tree: TrieNode::root(),
2752 post: vec![(
2753 DomainRule::Any,
2754 PathRule::Prefix("/forbidden".to_owned()),
2755 MethodRule::new(None),
2756 Route::Deny,
2757 )],
2758 };
2759
2760 for (label, hsts) in [
2761 ("none", None),
2762 (
2763 "disabled",
2764 Some(HstsConfig {
2765 enabled: Some(false),
2766 max_age: None,
2767 include_subdomains: None,
2768 preload: None,
2769 force_replace_backend: None,
2770 }),
2771 ),
2772 (
2773 "enabled-without-max-age",
2774 Some(HstsConfig {
2775 enabled: Some(true),
2776 max_age: None,
2777 include_subdomains: None,
2778 preload: None,
2779 force_replace_backend: None,
2780 }),
2781 ),
2782 ] {
2783 let mut router = make_router();
2784 let count = router.refresh_inheriting_hsts(hsts.as_ref());
2785 assert_eq!(count, 0, "no promotion expected for {label}");
2786 assert!(
2787 matches!(router.pre[0].3, Route::ClusterId(_)),
2788 "{label}: ClusterId must stay lightweight"
2789 );
2790 assert!(
2791 matches!(router.post[0].3, Route::Deny),
2792 "{label}: Deny must stay lightweight"
2793 );
2794 }
2795 }
2796
2797 #[test]
2798 fn refresh_inheriting_hsts_promoted_entry_refreshes_on_subsequent_patches() {
2799 use crate::router::pattern_trie::TrieNode;
2804 let mut router = Router {
2805 pre: Vec::new(),
2806 tree: TrieNode::root(),
2807 post: vec![(
2808 DomainRule::Any,
2809 PathRule::Prefix("/".to_owned()),
2810 MethodRule::new(None),
2811 Route::ClusterId("api".to_owned()),
2812 )],
2813 };
2814
2815 let first_patch = HstsConfig {
2816 enabled: Some(true),
2817 max_age: Some(31_536_000),
2818 include_subdomains: None,
2819 preload: None,
2820 force_replace_backend: None,
2821 };
2822 assert_eq!(router.refresh_inheriting_hsts(Some(&first_patch)), 1);
2823
2824 let second_patch = HstsConfig {
2825 enabled: Some(true),
2826 max_age: Some(63_072_000),
2827 include_subdomains: Some(true),
2828 preload: None,
2829 force_replace_backend: None,
2830 };
2831 assert_eq!(
2832 router.refresh_inheriting_hsts(Some(&second_patch)),
2833 1,
2834 "the previously promoted entry must be re-counted via the path-1 branch"
2835 );
2836
2837 let Route::Frontend(rc) = &router.post[0].3 else {
2838 panic!("post[0] should still be Route::Frontend after the second patch");
2839 };
2840 let response: Vec<_> = rc.headers_response.iter().collect();
2841 assert_eq!(
2842 response.len(),
2843 1,
2844 "second patch must REPLACE the existing STS edit, not append a duplicate"
2845 );
2846 assert_eq!(
2847 &*response[0].val,
2848 b"max-age=63072000; includeSubDomains".as_slice()
2849 );
2850 }
2851
2852 #[test]
2853 fn refresh_inheriting_hsts_promoted_entry_loses_hsts_on_disable_patch() {
2854 use crate::router::pattern_trie::TrieNode;
2861 let mut router = Router {
2862 pre: vec![(
2863 DomainRule::Any,
2864 PathRule::Prefix("/".to_owned()),
2865 MethodRule::new(None),
2866 Route::ClusterId("api".to_owned()),
2867 )],
2868 tree: TrieNode::root(),
2869 post: Vec::new(),
2870 };
2871
2872 let enable = HstsConfig {
2873 enabled: Some(true),
2874 max_age: Some(31_536_000),
2875 include_subdomains: None,
2876 preload: None,
2877 force_replace_backend: None,
2878 };
2879 assert_eq!(router.refresh_inheriting_hsts(Some(&enable)), 1);
2880
2881 let disable = HstsConfig {
2882 enabled: Some(false),
2883 max_age: None,
2884 include_subdomains: None,
2885 preload: None,
2886 force_replace_backend: None,
2887 };
2888 assert_eq!(
2889 router.refresh_inheriting_hsts(Some(&disable)),
2890 1,
2891 "the promoted entry must still be touched on disable to strip its STS edit"
2892 );
2893
2894 let Route::Frontend(rc) = &router.pre[0].3 else {
2895 panic!("pre[0] should still be Route::Frontend (no demotion)");
2896 };
2897 assert_eq!(rc.cluster_id.as_deref(), Some("api"));
2898 assert_eq!(
2899 rc.headers_response.len(),
2900 0,
2901 "disable patch must strip the STS edit from the promoted entry"
2902 );
2903 }
2904
2905 #[test]
2906 fn refresh_inheriting_hsts_promotes_clusterid_in_trie_on_enable() {
2907 use crate::router::pattern_trie::TrieNode;
2914 let mut router = Router {
2915 pre: Vec::new(),
2916 tree: TrieNode::root(),
2917 post: Vec::new(),
2918 };
2919 let path_rule = PathRule::Prefix("/".to_owned());
2920 let method_rule = MethodRule::new(None);
2921 assert!(router.add_tree_rule(
2922 b"example.com",
2923 &path_rule,
2924 &method_rule,
2925 &Route::ClusterId("api".to_owned()),
2926 ));
2927
2928 let new_hsts = HstsConfig {
2929 enabled: Some(true),
2930 max_age: Some(31_536_000),
2931 include_subdomains: Some(true),
2932 preload: None,
2933 force_replace_backend: None,
2934 };
2935 let count = router.refresh_inheriting_hsts(Some(&new_hsts));
2936 assert_eq!(
2937 count, 1,
2938 "trie-resident ClusterId must be promoted + counted"
2939 );
2940
2941 let (_, paths) = router
2942 .tree
2943 .domain_lookup_mut(b"example.com", false)
2944 .expect("trie leaf still present after refresh");
2945 assert_eq!(paths.len(), 1);
2946 let Route::Frontend(rc) = &paths[0].2 else {
2947 panic!("trie leaf should now be Route::Frontend, not Route::ClusterId");
2948 };
2949 assert_eq!(rc.cluster_id.as_deref(), Some("api"));
2950 assert_eq!(rc.redirect, RedirectPolicy::Forward);
2951 assert!(rc.inherits_listener_hsts);
2952 let response: Vec<_> = rc.headers_response.iter().collect();
2953 assert_eq!(response.len(), 1);
2954 assert_eq!(&*response[0].key, b"strict-transport-security");
2955 assert_eq!(
2956 &*response[0].val,
2957 b"max-age=31536000; includeSubDomains".as_slice()
2958 );
2959 }
2960
2961 #[test]
2962 fn convert_regex() {
2963 assert_eq!(
2966 convert_regex_domain_rule("www.example.com")
2967 .unwrap()
2968 .as_str(),
2969 "\\Awww\\.example\\.com\\z"
2970 );
2971 assert_eq!(
2972 convert_regex_domain_rule("*.example.com").unwrap().as_str(),
2973 "\\A*\\.example\\.com\\z"
2974 );
2975 assert_eq!(
2976 convert_regex_domain_rule("test.*.example.com")
2977 .unwrap()
2978 .as_str(),
2979 "\\Atest\\.*\\.example\\.com\\z"
2980 );
2981 assert_eq!(
2982 convert_regex_domain_rule("css./cdn[a-z0-9]+/.example.com")
2983 .unwrap()
2984 .as_str(),
2985 "\\Acss\\.cdn[a-z0-9]+\\.example\\.com\\z"
2986 );
2987
2988 assert_eq!(
2989 convert_regex_domain_rule("css./cdn[a-z0-9]+.example.com"),
2990 None
2991 );
2992 assert_eq!(
2993 convert_regex_domain_rule("css./cdn[a-z0-9]+/a.example.com"),
2994 None
2995 );
2996 }
2997
2998 #[test]
3003 fn regex_domain_rule_rejects_suffix_and_prefix() {
3004 let rule: DomainRule = "/example\\.com/".parse().unwrap();
3005 assert!(rule.matches(b"example.com"));
3006 assert!(!rule.matches(b"attacker.example.com"));
3007 assert!(!rule.matches(b"example.com.evil.org"));
3008 assert!(!rule.matches(b"prefixexample.com"));
3009 assert!(!rule.matches(b"example.commercial"));
3010 }
3011
3012 #[test]
3018 fn regex_domain_rule_multi_segment_segments_are_isolated() {
3019 let pattern = convert_regex_domain_rule("/seg1/.foo./seg2/.com")
3020 .expect("multi-segment regex hostname must compile");
3021 assert_eq!(pattern.as_str(), "\\Aseg1\\.foo\\.seg2\\.com\\z");
3022 }
3023
3024 #[test]
3025 fn parse_domain_rule() {
3026 assert_eq!("*".parse::<DomainRule>().unwrap(), DomainRule::Any);
3027 assert_eq!(
3028 "www.example.com".parse::<DomainRule>().unwrap(),
3029 DomainRule::Exact("www.example.com".to_string())
3030 );
3031 assert_eq!(
3032 "*.example.com".parse::<DomainRule>().unwrap(),
3033 DomainRule::Wildcard("*.example.com".to_string())
3034 );
3035 assert_eq!("test.*.example.com".parse::<DomainRule>(), Err(()));
3036 assert_eq!(
3037 "/cdn[0-9]+/.example.com".parse::<DomainRule>().unwrap(),
3038 DomainRule::Regex(Regex::new("\\Acdn[0-9]+\\.example\\.com\\z").unwrap())
3039 );
3040 }
3041
3042 #[test]
3043 fn match_domain_rule() {
3044 assert!(DomainRule::Any.matches("www.example.com".as_bytes()));
3045 assert!(
3046 DomainRule::Exact("www.example.com".to_string()).matches("www.example.com".as_bytes())
3047 );
3048 assert!(
3049 DomainRule::Wildcard("*.example.com".to_string()).matches("www.example.com".as_bytes())
3050 );
3051 assert!(
3052 !DomainRule::Wildcard("*.example.com".to_string())
3053 .matches("test.www.example.com".as_bytes())
3054 );
3055 assert!(
3056 "/cdn[0-9]+/.example.com"
3057 .parse::<DomainRule>()
3058 .unwrap()
3059 .matches("cdn1.example.com".as_bytes())
3060 );
3061 assert!(
3062 !"/cdn[0-9]+/.example.com"
3063 .parse::<DomainRule>()
3064 .unwrap()
3065 .matches("www.example.com".as_bytes())
3066 );
3067 assert!(
3068 !"/cdn[0-9]+/.example.com"
3069 .parse::<DomainRule>()
3070 .unwrap()
3071 .matches("cdn10.exampleAcom".as_bytes())
3072 );
3073 }
3074
3075 #[test]
3076 fn match_domain_rule_wildcard_short_hostname_does_not_panic() {
3077 let rule = DomainRule::Wildcard("*.foo.example.com".to_string());
3078
3079 assert!(!rule.matches(b""));
3081
3082 assert!(!rule.matches(b"a.b"));
3084 assert!(!rule.matches(b"x"));
3085
3086 assert!(!rule.matches(b".foo.example.com"));
3089
3090 assert!(!rule.matches(b"y.x.foo.example.com"));
3092
3093 assert!(rule.matches(b"x.foo.example.com"));
3096 }
3097
3098 #[test]
3099 fn router_lookup_wildcard_pre_rule_short_hostname_does_not_panic() {
3100 let mut router = Router::new();
3101
3102 assert!(router.add_pre_rule(
3105 &"*.foo.example.com".parse::<DomainRule>().unwrap(),
3106 &PathRule::Prefix("/".to_string()),
3107 &MethodRule::new(Some("GET".to_string())),
3108 &Route::ClusterId("wildcard".to_string()),
3109 ));
3110
3111 let method = Method::new(&b"GET"[..]);
3112
3113 assert!(router.lookup("", "/", &method).is_err());
3115 assert!(router.lookup("x", "/", &method).is_err());
3116 assert!(router.lookup("a.b", "/", &method).is_err());
3117
3118 assert!(router.lookup(".foo.example.com", "/", &method).is_err());
3120
3121 assert_eq!(
3123 router.lookup("x.foo.example.com", "/", &method),
3124 Ok(RouteResult::forward("wildcard".to_string()))
3125 );
3126 }
3127
3128 #[test]
3129 fn match_path_rule() {
3130 assert!(PathRule::Prefix("".to_string()).matches("/".as_bytes()) != PathRuleResult::None);
3131 assert!(
3132 PathRule::Prefix("".to_string()).matches("/hello".as_bytes()) != PathRuleResult::None
3133 );
3134 assert!(
3135 PathRule::Prefix("/hello".to_string()).matches("/hello".as_bytes())
3136 != PathRuleResult::None
3137 );
3138 assert!(
3139 PathRule::Prefix("/hello".to_string()).matches("/hello/world".as_bytes())
3140 != PathRuleResult::None
3141 );
3142 assert!(
3143 PathRule::Prefix("/hello".to_string()).matches("/".as_bytes()) == PathRuleResult::None
3144 );
3145 }
3146
3147 #[test]
3155 fn multiple_children_on_a_wildcard() {
3156 let mut router = Router::new();
3157
3158 assert!(router.add_tree_rule(
3159 b"*.sozu.io",
3160 &PathRule::Prefix("".to_string()),
3161 &MethodRule::new(Some("GET".to_string())),
3162 &Route::ClusterId("base".to_string())
3163 ));
3164 println!("{:#?}", router.tree);
3165 assert_eq!(
3166 router.lookup("www.sozu.io", "/api", &Method::Get),
3167 Ok(RouteResult::forward("base".to_string()))
3168 );
3169 assert!(router.add_tree_rule(
3170 b"*.sozu.io",
3171 &PathRule::Prefix("/api".to_string()),
3172 &MethodRule::new(Some("GET".to_string())),
3173 &Route::ClusterId("api".to_string())
3174 ));
3175 println!("{:#?}", router.tree);
3176 assert_eq!(
3177 router.lookup("www.sozu.io", "/ap", &Method::Get),
3178 Ok(RouteResult::forward("base".to_string()))
3179 );
3180 assert_eq!(
3181 router.lookup("www.sozu.io", "/api", &Method::Get),
3182 Ok(RouteResult::forward("api".to_string()))
3183 );
3184 }
3185
3186 #[test]
3194 fn multiple_children_including_one_with_wildcard() {
3195 let mut router = Router::new();
3196
3197 assert!(router.add_tree_rule(
3198 b"*.sozu.io",
3199 &PathRule::Prefix("".to_string()),
3200 &MethodRule::new(Some("GET".to_string())),
3201 &Route::ClusterId("base".to_string())
3202 ));
3203 println!("{:#?}", router.tree);
3204 assert_eq!(
3205 router.lookup("www.sozu.io", "/api", &Method::Get),
3206 Ok(RouteResult::forward("base".to_string()))
3207 );
3208 assert!(router.add_tree_rule(
3209 b"api.sozu.io",
3210 &PathRule::Prefix("".to_string()),
3211 &MethodRule::new(Some("GET".to_string())),
3212 &Route::ClusterId("api".to_string())
3213 ));
3214 println!("{:#?}", router.tree);
3215 assert_eq!(
3216 router.lookup("www.sozu.io", "/api", &Method::Get),
3217 Ok(RouteResult::forward("base".to_string()))
3218 );
3219 assert_eq!(
3220 router.lookup("api.sozu.io", "/api", &Method::Get),
3221 Ok(RouteResult::forward("api".to_string()))
3222 );
3223 }
3224
3225 #[test]
3232 fn add_tree_rule_rejects_malformed_hostnames_without_panicking() {
3233 for hostname in [
3236 &b"example.com/"[..],
3237 b"www.example.com/",
3238 b"foo/",
3239 b"a/*/",
3240 b"/",
3241 b"///",
3242 b"abc/[0-9]+/.example.com",
3243 b"/[/.example.com",
3244 b".example.com",
3245 b".a.b",
3246 b"..",
3247 ] {
3248 let mut router = Router::new();
3249 assert!(
3250 !router.add_tree_rule(
3251 hostname,
3252 &PathRule::Prefix("/".to_string()),
3253 &MethodRule::new(Some("GET".to_string())),
3254 &Route::ClusterId("cluster".to_string()),
3255 ),
3256 "{:?} must be rejected, not inserted",
3257 String::from_utf8_lossy(hostname),
3258 );
3259 assert!(
3262 router.tree.is_empty(),
3263 "{:?} was rejected but still mutated the route table",
3264 String::from_utf8_lossy(hostname),
3265 );
3266 }
3267 }
3268
3269 #[test]
3273 fn add_http_front_surfaces_a_malformed_hostname_as_an_error() {
3274 let mut router = Router::new();
3275 let front = HttpFrontend {
3276 hostname: "example.com/".to_owned(),
3277 ..test_http_frontend()
3278 };
3279 assert!(matches!(
3280 router.add_http_front(&front),
3281 Err(RouterError::AddRoute(_))
3282 ));
3283 assert!(router.tree.is_empty());
3284
3285 for hostname in ["/a/.", "a/b/.", "x./y/."] {
3290 let mut router = Router::new();
3291 let front = HttpFrontend {
3292 hostname: (*hostname).to_owned(),
3293 ..test_http_frontend()
3294 };
3295 assert!(
3296 matches!(
3297 router.add_http_front(&front),
3298 Err(RouterError::InvalidDomain { .. })
3299 ),
3300 "{hostname:?} must be rejected as an invalid domain, not panic",
3301 );
3302 assert!(router.tree.is_empty());
3303 }
3304 }
3305
3306 #[test]
3315 fn domain_rule_rejects_a_trailing_dot_after_a_regex_segment() {
3316 for hostname in ["/a/.", "a/b/.", "/[/.", "x./y/."] {
3317 assert!(
3318 hostname.parse::<DomainRule>().is_err(),
3319 "{hostname:?} must be rejected, not panic",
3320 );
3321 }
3322 assert!("abc./[0-9]+/.example.com".parse::<DomainRule>().is_ok());
3325 }
3326
3327 #[test]
3335 fn add_http_front_rejects_an_oversized_hostname() {
3336 let mut router = Router::new();
3338 let front = HttpFrontend {
3339 hostname: "a".repeat(MAX_HOSTNAME_LENGTH),
3340 ..test_http_frontend()
3341 };
3342 assert!(router.add_http_front(&front).is_ok());
3343
3344 let mut router = Router::new();
3346 let front = HttpFrontend {
3347 hostname: "a".repeat(MAX_HOSTNAME_LENGTH + 1),
3348 ..test_http_frontend()
3349 };
3350 assert!(matches!(
3351 router.add_http_front(&front),
3352 Err(RouterError::InvalidDomain { .. })
3353 ));
3354 assert!(matches!(
3355 router.remove_http_front(&front),
3356 Err(RouterError::InvalidDomain { .. })
3357 ));
3358 assert!(router.tree.is_empty());
3359
3360 let front = HttpFrontend {
3362 hostname: "a.".repeat(MAX_HOSTNAME_LENGTH),
3363 ..test_http_frontend()
3364 };
3365 assert!(matches!(
3366 router.add_http_front(&front),
3367 Err(RouterError::InvalidDomain { .. })
3368 ));
3369 }
3370
3371 #[test]
3372 fn router_insert_remove_through_regex() {
3373 let mut router = Router::new();
3374
3375 assert!(router.add_tree_rule(
3376 b"www./.*/.io",
3377 &PathRule::Prefix("".to_string()),
3378 &MethodRule::new(Some("GET".to_string())),
3379 &Route::ClusterId("base".to_string())
3380 ));
3381 println!("{:#?}", router.tree);
3382 assert!(router.add_tree_rule(
3383 b"www.doc./.*/.io",
3384 &PathRule::Prefix("".to_string()),
3385 &MethodRule::new(Some("GET".to_string())),
3386 &Route::ClusterId("doc".to_string())
3387 ));
3388 println!("{:#?}", router.tree);
3389 assert_eq!(
3390 router.lookup("www.sozu.io", "/", &Method::Get),
3391 Ok(RouteResult::forward("base".to_string()))
3392 );
3393 assert_eq!(
3394 router.lookup("www.doc.sozu.io", "/", &Method::Get),
3395 Ok(RouteResult::forward("doc".to_string()))
3396 );
3397 assert!(router.remove_tree_rule(
3398 b"www./.*/.io",
3399 &PathRule::Prefix("".to_string()),
3400 &MethodRule::new(Some("GET".to_string()))
3401 ));
3402 println!("{:#?}", router.tree);
3403 assert!(router.lookup("www.sozu.io", "/", &Method::Get).is_err());
3404 assert_eq!(
3405 router.lookup("www.doc.sozu.io", "/", &Method::Get),
3406 Ok(RouteResult::forward("doc".to_string()))
3407 );
3408 }
3409
3410 #[test]
3411 fn match_router() {
3412 let mut router = Router::new();
3413
3414 assert!(router.add_pre_rule(
3415 &"*".parse::<DomainRule>().unwrap(),
3416 &PathRule::Prefix("/.well-known/acme-challenge".to_string()),
3417 &MethodRule::new(Some("GET".to_string())),
3418 &Route::ClusterId("acme".to_string())
3419 ));
3420 assert!(router.add_tree_rule(
3421 "www.example.com".as_bytes(),
3422 &PathRule::Prefix("/".to_string()),
3423 &MethodRule::new(Some("GET".to_string())),
3424 &Route::ClusterId("example".to_string())
3425 ));
3426 assert!(router.add_tree_rule(
3427 "*.test.example.com".as_bytes(),
3428 &PathRule::Regex(Regex::new("/hello[A-Z]+/").unwrap()),
3429 &MethodRule::new(Some("GET".to_string())),
3430 &Route::ClusterId("examplewildcard".to_string())
3431 ));
3432 assert!(router.add_tree_rule(
3433 "/test[0-9]/.example.com".as_bytes(),
3434 &PathRule::Prefix("/".to_string()),
3435 &MethodRule::new(Some("GET".to_string())),
3436 &Route::ClusterId("exampleregex".to_string())
3437 ));
3438
3439 assert_eq!(
3440 router.lookup("www.example.com", "/helloA", &Method::new(&b"GET"[..])),
3441 Ok(RouteResult::forward("example".to_string()))
3442 );
3443 assert_eq!(
3444 router.lookup(
3445 "www.example.com",
3446 "/.well-known/acme-challenge",
3447 &Method::new(&b"GET"[..])
3448 ),
3449 Ok(RouteResult::forward("acme".to_string()))
3450 );
3451 assert!(
3452 router
3453 .lookup("www.test.example.com", "/", &Method::new(&b"GET"[..]))
3454 .is_err()
3455 );
3456 assert_eq!(
3457 router.lookup(
3458 "www.test.example.com",
3459 "/helloAB/",
3460 &Method::new(&b"GET"[..])
3461 ),
3462 Ok(RouteResult::forward("examplewildcard".to_string()))
3463 );
3464 assert_eq!(
3465 router.lookup("test1.example.com", "/helloAB/", &Method::new(&b"GET"[..])),
3466 Ok(RouteResult::forward("exampleregex".to_string()))
3467 );
3468 }
3469
3470 #[test]
3471 fn has_hostname_checks_tree_pre_and_post() {
3472 let mut router = Router::new();
3473
3474 assert!(!router.has_hostname("www.example.com"));
3476
3477 assert!(router.add_tree_rule(
3479 b"www.example.com",
3480 &PathRule::Prefix("/".to_string()),
3481 &MethodRule::new(Some("GET".to_string())),
3482 &Route::ClusterId("cluster1".to_string())
3483 ));
3484 assert!(router.has_hostname("www.example.com"));
3485 assert!(!router.has_hostname("api.example.com"));
3486
3487 assert!(router.remove_tree_rule(
3489 b"www.example.com",
3490 &PathRule::Prefix("/".to_string()),
3491 &MethodRule::new(Some("GET".to_string()))
3492 ));
3493 assert!(!router.has_hostname("www.example.com"));
3494
3495 assert!(router.add_pre_rule(
3497 &DomainRule::Exact("api.example.com".to_string()),
3498 &PathRule::Prefix("/".to_string()),
3499 &MethodRule::new(None),
3500 &Route::ClusterId("cluster2".to_string())
3501 ));
3502 assert!(router.has_hostname("api.example.com"));
3503 assert!(!router.has_hostname("www.example.com"));
3504
3505 assert!(router.add_post_rule(
3507 &DomainRule::Exact("cdn.example.com".to_string()),
3508 &PathRule::Prefix("/".to_string()),
3509 &MethodRule::new(None),
3510 &Route::ClusterId("cluster3".to_string())
3511 ));
3512 assert!(router.has_hostname("cdn.example.com"));
3513
3514 assert!(router.remove_pre_rule(
3516 &DomainRule::Exact("api.example.com".to_string()),
3517 &PathRule::Prefix("/".to_string()),
3518 &MethodRule::new(None),
3519 ));
3520 assert!(!router.has_hostname("api.example.com"));
3521 assert!(router.has_hostname("cdn.example.com"));
3522 }
3523
3524 #[test]
3525 fn has_hostname_false_after_last_route_removed() {
3526 let mut router = Router::new();
3527
3528 assert!(router.add_tree_rule(
3530 b"www.example.com",
3531 &PathRule::Prefix("/".to_string()),
3532 &MethodRule::new(Some("GET".to_string())),
3533 &Route::ClusterId("cluster1".to_string())
3534 ));
3535 assert!(router.add_tree_rule(
3536 b"www.example.com",
3537 &PathRule::Prefix("/api".to_string()),
3538 &MethodRule::new(Some("GET".to_string())),
3539 &Route::ClusterId("cluster2".to_string())
3540 ));
3541 assert!(router.has_hostname("www.example.com"));
3542
3543 assert!(router.remove_tree_rule(
3545 b"www.example.com",
3546 &PathRule::Prefix("/".to_string()),
3547 &MethodRule::new(Some("GET".to_string()))
3548 ));
3549 assert!(router.has_hostname("www.example.com"));
3550
3551 assert!(router.remove_tree_rule(
3553 b"www.example.com",
3554 &PathRule::Prefix("/api".to_string()),
3555 &MethodRule::new(Some("GET".to_string()))
3556 ));
3557 assert!(!router.has_hostname("www.example.com"));
3558 }
3559}