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::{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
41#[derive(thiserror::Error, Debug, PartialEq)]
42pub enum RouterError {
43 #[error("Could not parse rule from frontend path {0:?}")]
44 InvalidPathRule(String),
45 #[error("parsing hostname {hostname} failed")]
46 InvalidDomain { hostname: String },
47 #[error("Could not parse host rewrite {0:?}")]
48 InvalidHostRewrite(String),
49 #[error("Could not parse path rewrite {0:?}")]
50 InvalidPathRewrite(String),
51 #[error("Could not add route {0}")]
52 AddRoute(String),
53 #[error("Could not remove route {0}")]
54 RemoveRoute(String),
55 #[error("no route for {method} {host} {path}")]
56 RouteNotFound {
57 host: String,
58 path: String,
59 method: Method,
60 },
61}
62
63pub struct Router {
64 pre: Vec<(DomainRule, PathRule, MethodRule, Route)>,
65 pub tree: TrieNode<Vec<(PathRule, MethodRule, Route)>>,
66 post: Vec<(DomainRule, PathRule, MethodRule, Route)>,
67}
68
69impl Default for Router {
70 fn default() -> Self {
71 Self::new()
72 }
73}
74
75impl Router {
76 pub fn new() -> Router {
77 Router {
78 pre: Vec::new(),
79 tree: TrieNode::root(),
80 post: Vec::new(),
81 }
82 }
83
84 pub fn lookup(
93 &self,
94 hostname: &str,
95 path: &str,
96 method: &Method,
97 ) -> Result<RouteResult, RouterError> {
98 let hostname_b = hostname.as_bytes();
99 let path_b = path.as_bytes();
100 for (domain_rule, path_rule, method_rule, route) in &self.pre {
101 if domain_rule.matches(hostname_b)
102 && path_rule.matches(path_b) != PathRuleResult::None
103 && method_rule.matches(method) != MethodRuleResult::None
104 {
105 return Ok(RouteResult::new_no_trie(
106 hostname_b,
107 domain_rule,
108 path_b,
109 path_rule,
110 route,
111 ));
112 }
113 }
114
115 let trie_path: TrieMatches<'_, '_> = Vec::with_capacity(16);
116 if let Some(((_, path_rules), trie_matches)) =
117 self.tree.lookup_with_path(hostname_b, true, trie_path)
118 {
119 let mut prefix_length = 0;
120 let mut matched: Option<(&PathRule, &Route)> = None;
121
122 for (rule, method_rule, route) in path_rules {
123 match rule.matches(path_b) {
124 PathRuleResult::Regex | PathRuleResult::Equals => {
125 match method_rule.matches(method) {
126 MethodRuleResult::Equals => {
127 return Ok(RouteResult::new_with_trie(
128 hostname_b,
129 trie_matches,
130 path_b,
131 rule,
132 route,
133 ));
134 }
135 MethodRuleResult::All => {
136 prefix_length = path_b.len();
137 matched = Some((rule, route));
138 }
139 MethodRuleResult::None => {}
140 }
141 }
142 PathRuleResult::Prefix(size) => {
143 if size >= prefix_length {
144 match method_rule.matches(method) {
145 MethodRuleResult::Equals => {
147 debug_assert!(
151 size >= prefix_length,
152 "longest-prefix selection must never shrink the match length",
153 );
154 prefix_length = size;
155 matched = Some((rule, route));
156 }
157 MethodRuleResult::All => {
158 debug_assert!(
159 size >= prefix_length,
160 "longest-prefix selection must never shrink the match length",
161 );
162 prefix_length = size;
163 matched = Some((rule, route));
164 }
165 MethodRuleResult::None => {}
166 }
167 }
168 }
169 PathRuleResult::None => {}
170 }
171 }
172
173 if let Some((path_rule, route)) = matched {
174 return Ok(RouteResult::new_with_trie(
175 hostname_b,
176 trie_matches,
177 path_b,
178 path_rule,
179 route,
180 ));
181 }
182 }
183
184 for (domain_rule, path_rule, method_rule, route) in self.post.iter() {
185 if domain_rule.matches(hostname_b)
186 && path_rule.matches(path_b) != PathRuleResult::None
187 && method_rule.matches(method) != MethodRuleResult::None
188 {
189 return Ok(RouteResult::new_no_trie(
190 hostname_b,
191 domain_rule,
192 path_b,
193 path_rule,
194 route,
195 ));
196 }
197 }
198
199 Err(RouterError::RouteNotFound {
200 host: hostname.to_owned(),
201 path: path.to_owned(),
202 method: method.to_owned(),
203 })
204 }
205
206 pub fn add_http_front(&mut self, front: &HttpFrontend) -> Result<(), RouterError> {
213 self.add_http_front_with_hsts_origin(front, HstsOrigin::Explicit)
214 }
215
216 pub fn add_http_front_with_hsts_origin(
223 &mut self,
224 front: &HttpFrontend,
225 hsts_origin: HstsOrigin,
226 ) -> Result<(), RouterError> {
227 let path_rule = PathRule::from_config(front.path.clone())
228 .ok_or(RouterError::InvalidPathRule(front.path.to_string()))?;
229
230 let method_rule = MethodRule::new(front.method.clone());
231
232 let has_policy = front.redirect.is_some()
237 || front.redirect_scheme.is_some()
238 || front.redirect_template.is_some()
239 || front.rewrite_host.is_some()
240 || front.rewrite_path.is_some()
241 || front.rewrite_port.is_some()
242 || front.required_auth.unwrap_or(false)
243 || !front.headers.is_empty()
244 || front.hsts.is_some();
245
246 let domain =
247 front
248 .hostname
249 .parse::<DomainRule>()
250 .map_err(|_| RouterError::InvalidDomain {
251 hostname: front.hostname.clone(),
252 })?;
253
254 let route = if has_policy {
255 let redirect = front
256 .redirect
257 .and_then(|r| RedirectPolicy::try_from(r).ok())
258 .unwrap_or(RedirectPolicy::Forward);
259 let redirect_scheme = front
260 .redirect_scheme
261 .and_then(|s| RedirectScheme::try_from(s).ok())
262 .unwrap_or(RedirectScheme::UseSame);
263 let frontend = Frontend::new(
264 &domain,
265 &path_rule,
266 front,
267 redirect,
268 redirect_scheme,
269 front.redirect_template.clone(),
270 front.rewrite_host.clone(),
271 front.rewrite_path.clone(),
272 front.rewrite_port.and_then(|p| u16::try_from(p).ok()),
273 &front.headers,
274 front.required_auth.unwrap_or(false),
275 hsts_origin,
276 )?;
277 Route::Frontend(Rc::new(frontend))
278 } else {
279 match &front.cluster_id {
280 Some(cluster_id) => Route::ClusterId(cluster_id.clone()),
281 None => Route::Deny,
282 }
283 };
284
285 let success = match front.position {
286 RulePosition::Pre => self.add_pre_rule(&domain, &path_rule, &method_rule, &route),
287 RulePosition::Post => self.add_post_rule(&domain, &path_rule, &method_rule, &route),
288 RulePosition::Tree => {
289 self.add_tree_rule(front.hostname.as_bytes(), &path_rule, &method_rule, &route)
290 }
291 };
292 if !success {
293 return Err(RouterError::AddRoute(format!("{front:?}")));
294 }
295 Ok(())
296 }
297
298 pub fn remove_http_front(&mut self, front: &HttpFrontend) -> Result<(), RouterError> {
299 let path_rule = PathRule::from_config(front.path.clone())
300 .ok_or(RouterError::InvalidPathRule(front.path.to_string()))?;
301
302 let method_rule = MethodRule::new(front.method.clone());
303
304 let remove_success = match front.position {
305 RulePosition::Pre => {
306 let domain = front.hostname.parse::<DomainRule>().map_err(|_| {
307 RouterError::InvalidDomain {
308 hostname: front.hostname.clone(),
309 }
310 })?;
311
312 self.remove_pre_rule(&domain, &path_rule, &method_rule)
313 }
314 RulePosition::Post => {
315 let domain = front.hostname.parse::<DomainRule>().map_err(|_| {
316 RouterError::InvalidDomain {
317 hostname: front.hostname.clone(),
318 }
319 })?;
320
321 self.remove_post_rule(&domain, &path_rule, &method_rule)
322 }
323 RulePosition::Tree => {
324 self.remove_tree_rule(front.hostname.as_bytes(), &path_rule, &method_rule)
325 }
326 };
327 if !remove_success {
328 return Err(RouterError::RemoveRoute(format!("{front:?}")));
329 }
330 Ok(())
331 }
332
333 pub fn add_tree_rule(
334 &mut self,
335 hostname: &[u8],
336 path: &PathRule,
337 method: &MethodRule,
338 cluster: &Route,
339 ) -> bool {
340 let hostname = match from_utf8(hostname) {
341 Err(_) => return false,
342 Ok(h) => h,
343 };
344
345 match ::idna::domain_to_ascii(hostname) {
346 Ok(hostname) => {
347 let mut empty = true;
349 if let Some((_, paths)) = self.tree.domain_lookup_mut(hostname.as_bytes(), false) {
350 empty = false;
351 let before = paths.len();
352 if !paths.iter().any(|(p, m, _)| p == path && m == method) {
353 paths.push((path.to_owned(), method.to_owned(), cluster.to_owned()));
354 debug_assert_eq!(
357 paths.len(),
358 before + 1,
359 "appending a tree rule must grow the leaf's rule list by exactly one",
360 );
361 debug_assert!(
362 paths.iter().any(|(p, m, _)| p == path && m == method),
363 "the freshly appended (path, method) rule must be present after insert",
364 );
365 return true;
366 }
367 }
368
369 if empty {
370 let inserted_host = hostname.clone().into_bytes();
375 self.tree.domain_insert(
376 hostname.into_bytes(),
377 vec![(path.to_owned(), method.to_owned(), cluster.to_owned())],
378 );
379 debug_assert!(
388 self.tree
389 .domain_lookup_mut(&inserted_host, false)
390 .is_some_and(|(_, paths)| paths
391 .iter()
392 .any(|(p, m, _)| p == path && m == method)),
393 "a freshly inserted tree domain must resolve to its inserted rule",
394 );
395 return true;
396 }
397
398 false
399 }
400 Err(_) => false,
401 }
402 }
403
404 pub fn remove_tree_rule(
405 &mut self,
406 hostname: &[u8],
407 path: &PathRule,
408 method: &MethodRule,
409 ) -> bool {
411 let hostname = match from_utf8(hostname) {
412 Err(_) => return false,
413 Ok(h) => h,
414 };
415
416 match ::idna::domain_to_ascii(hostname) {
417 Ok(hostname) => {
418 let should_delete = {
419 let paths_opt = self.tree.domain_lookup_mut(hostname.as_bytes(), false);
420
421 if let Some((_, paths)) = paths_opt {
422 paths.retain(|(p, m, _)| p != path || m != method);
423 debug_assert!(
426 !paths.iter().any(|(p, m, _)| p == path && m == method),
427 "remove must evict every matching (path, method) rule from the leaf",
428 );
429 }
430
431 paths_opt
432 .as_ref()
433 .map(|(_, paths)| paths.is_empty())
434 .unwrap_or(false)
435 };
436
437 if should_delete {
438 let removed_host = hostname.clone().into_bytes();
439 self.tree.domain_remove(&hostname.into_bytes());
440 debug_assert!(
448 self.tree.domain_lookup_mut(&removed_host, false).is_none(),
449 "a domain whose last rule was removed must be unreachable",
450 );
451 }
452
453 true
454 }
455 Err(_) => false,
456 }
457 }
458
459 pub fn refresh_inheriting_hsts(&mut self, new_hsts: Option<&HstsConfig>) -> usize {
503 let mut refreshed = 0usize;
504 let new_edit = build_listener_hsts_edit(new_hsts);
512 let new_edit_ref = new_edit.as_ref();
513 let promote_lightweight = new_edit_ref.is_some();
514 let mut visit = |route: &mut Route| match route {
515 Route::Frontend(rc) => {
516 if rc.inherits_listener_hsts {
517 let new_frontend = rebuild_with_listener_hsts(rc, new_edit_ref);
518 *rc = Rc::new(new_frontend);
519 refreshed += 1;
520 }
521 }
522 Route::ClusterId(id) => {
523 if promote_lightweight {
524 let promoted = rebuild_with_listener_hsts(
525 &Frontend::minimal_forward(id.clone()),
526 new_edit_ref,
527 );
528 *route = Route::Frontend(Rc::new(promoted));
529 refreshed += 1;
530 }
531 }
532 Route::Deny => {
533 if promote_lightweight {
534 let promoted =
535 rebuild_with_listener_hsts(&Frontend::minimal_deny(), new_edit_ref);
536 *route = Route::Frontend(Rc::new(promoted));
537 refreshed += 1;
538 }
539 }
540 };
541
542 for (_, _, _, route) in self.pre.iter_mut() {
543 visit(route);
544 }
545 self.tree.for_each_value_mut(&mut |paths| {
546 for (_, _, route) in paths.iter_mut() {
547 visit(route);
548 }
549 });
550 for (_, _, _, route) in self.post.iter_mut() {
551 visit(route);
552 }
553 refreshed
554 }
555
556 pub fn add_pre_rule(
557 &mut self,
558 domain: &DomainRule,
559 path: &PathRule,
560 method: &MethodRule,
561 cluster_id: &Route,
562 ) -> bool {
563 let before = self.pre.len();
564 if !self
565 .pre
566 .iter()
567 .any(|(d, p, m, _)| d == domain && p == path && m == method)
568 {
569 self.pre.push((
570 domain.to_owned(),
571 path.to_owned(),
572 method.to_owned(),
573 cluster_id.to_owned(),
574 ));
575 debug_assert_eq!(
579 self.pre.len(),
580 before + 1,
581 "adding a unique pre-rule must push exactly one entry",
582 );
583 debug_assert!(
584 self.pre
585 .iter()
586 .any(|(d, p, m, _)| d == domain && p == path && m == method),
587 "the freshly added pre-rule must be present",
588 );
589 true
590 } else {
591 debug_assert_eq!(
592 self.pre.len(),
593 before,
594 "a duplicate pre-rule must not change the list length",
595 );
596 false
597 }
598 }
599
600 pub fn add_post_rule(
601 &mut self,
602 domain: &DomainRule,
603 path: &PathRule,
604 method: &MethodRule,
605 cluster_id: &Route,
606 ) -> bool {
607 let before = self.post.len();
608 if !self
609 .post
610 .iter()
611 .any(|(d, p, m, _)| d == domain && p == path && m == method)
612 {
613 self.post.push((
614 domain.to_owned(),
615 path.to_owned(),
616 method.to_owned(),
617 cluster_id.to_owned(),
618 ));
619 debug_assert_eq!(
620 self.post.len(),
621 before + 1,
622 "adding a unique post-rule must push exactly one entry",
623 );
624 debug_assert!(
625 self.post
626 .iter()
627 .any(|(d, p, m, _)| d == domain && p == path && m == method),
628 "the freshly added post-rule must be present",
629 );
630 true
631 } else {
632 debug_assert_eq!(
633 self.post.len(),
634 before,
635 "a duplicate post-rule must not change the list length",
636 );
637 false
638 }
639 }
640
641 pub fn remove_pre_rule(
642 &mut self,
643 domain: &DomainRule,
644 path: &PathRule,
645 method: &MethodRule,
646 ) -> bool {
647 let before = self.pre.len();
648 match self
649 .pre
650 .iter()
651 .position(|(d, p, m, _)| d == domain && p == path && m == method)
652 {
653 None => {
654 debug_assert_eq!(
655 self.pre.len(),
656 before,
657 "a no-op pre-rule removal must not change the list length",
658 );
659 false
660 }
661 Some(index) => {
662 debug_assert!(index < self.pre.len(), "found index must be in bounds");
663 self.pre.remove(index);
664 debug_assert_eq!(
666 self.pre.len() + 1,
667 before,
668 "removing a pre-rule must drop exactly one entry",
669 );
670 debug_assert!(
671 !self
672 .pre
673 .iter()
674 .any(|(d, p, m, _)| d == domain && p == path && m == method),
675 "the removed pre-rule must no longer be present",
676 );
677 true
678 }
679 }
680 }
681
682 pub fn remove_post_rule(
683 &mut self,
684 domain: &DomainRule,
685 path: &PathRule,
686 method: &MethodRule,
687 ) -> bool {
688 let before = self.post.len();
689 match self
690 .post
691 .iter()
692 .position(|(d, p, m, _)| d == domain && p == path && m == method)
693 {
694 None => {
695 debug_assert_eq!(
696 self.post.len(),
697 before,
698 "a no-op post-rule removal must not change the list length",
699 );
700 false
701 }
702 Some(index) => {
703 debug_assert!(index < self.post.len(), "found index must be in bounds");
704 self.post.remove(index);
705 debug_assert_eq!(
706 self.post.len() + 1,
707 before,
708 "removing a post-rule must drop exactly one entry",
709 );
710 debug_assert!(
711 !self
712 .post
713 .iter()
714 .any(|(d, p, m, _)| d == domain && p == path && m == method),
715 "the removed post-rule must no longer be present",
716 );
717 true
718 }
719 }
720 }
721
722 pub fn has_hostname(&self, hostname: &str) -> bool {
727 let hostname_b = hostname.as_bytes();
728
729 for (domain_rule, _, _, _) in &self.pre {
731 if domain_rule.matches(hostname_b) {
732 return true;
733 }
734 }
735
736 if let Ok(ascii_hostname) = ::idna::domain_to_ascii(hostname)
738 && self
739 .tree
740 .domain_lookup(ascii_hostname.as_bytes(), false)
741 .is_some()
742 {
743 return true;
744 }
745
746 for (domain_rule, _, _, _) in &self.post {
748 if domain_rule.matches(hostname_b) {
749 return true;
750 }
751 }
752
753 false
754 }
755}
756
757#[derive(Clone, Debug)]
758pub enum DomainRule {
759 Any,
760 Exact(String),
761 Wildcard(String),
767 Regex(Regex),
768}
769
770fn convert_regex_domain_rule(hostname: &str) -> Option<String> {
771 let mut result = String::from("\\A");
777
778 let s = hostname.as_bytes();
779 let mut index = 0;
780 loop {
781 if s[index] == b'/' {
782 let mut found = false;
783 for i in index + 1..s.len() {
784 if s[i] == b'/' {
785 match std::str::from_utf8(&s[index + 1..i]) {
786 Ok(r) => result.push_str(r),
787 Err(_) => return None,
788 }
789 index = i + 1;
790 found = true;
791 break;
792 }
793 }
794
795 if !found {
796 return None;
797 }
798 } else {
799 let start = index;
800 for i in start..s.len() + 1 {
801 index = i;
802 if i < s.len() && s[i] == b'.' {
803 match std::str::from_utf8(&s[start..i]) {
804 Ok(r) => result.push_str(r),
805 Err(_) => return None,
806 }
807 break;
808 }
809 }
810 if index == s.len() {
811 match std::str::from_utf8(&s[start..]) {
812 Ok(r) => result.push_str(r),
813 Err(_) => return None,
814 }
815 }
816 }
817
818 if index == s.len() {
819 result.push_str("\\z");
820 return Some(result);
821 } else if s[index] == b'.' {
822 result.push_str("\\.");
823 index += 1;
824 } else {
825 return None;
826 }
827 }
828}
829
830impl DomainRule {
831 pub fn matches(&self, hostname: &[u8]) -> bool {
832 match self {
833 DomainRule::Any => true,
834 DomainRule::Wildcard(s) => {
835 debug_assert_eq!(
839 s.as_bytes().first(),
840 Some(&b'*'),
841 "a Wildcard rule must retain its leading '*'",
842 );
843 let suffix = &s.as_bytes()[1..];
844 let matched = hostname
845 .strip_suffix(suffix)
846 .is_some_and(|prefix| !prefix.is_empty() && !prefix.contains(&b'.'));
847 debug_assert!(
851 !matched || hostname.len() > suffix.len(),
852 "a wildcard match requires a non-empty leftmost label before the suffix",
853 );
854 matched
855 }
856 DomainRule::Exact(s) => s.as_bytes() == hostname,
857 DomainRule::Regex(r) => {
858 let start = Instant::now();
859 let is_a_match = r.is_match(hostname);
860 let now = Instant::now();
861 time!(
862 names::event_loop::REGEX_MATCHING_TIME,
863 (now - start).as_millis()
864 );
865 is_a_match
866 }
867 }
868 }
869}
870
871impl std::cmp::PartialEq for DomainRule {
872 fn eq(&self, other: &Self) -> bool {
873 match (self, other) {
874 (DomainRule::Any, DomainRule::Any) => true,
875 (DomainRule::Wildcard(s1), DomainRule::Wildcard(s2)) => s1 == s2,
876 (DomainRule::Exact(s1), DomainRule::Exact(s2)) => s1 == s2,
877 (DomainRule::Regex(r1), DomainRule::Regex(r2)) => r1.as_str() == r2.as_str(),
878 _ => false,
879 }
880 }
881}
882
883impl std::str::FromStr for DomainRule {
884 type Err = ();
885
886 fn from_str(s: &str) -> Result<Self, Self::Err> {
887 Ok(if s == "*" {
888 DomainRule::Any
889 } else if s.contains('/') {
890 match convert_regex_domain_rule(s) {
891 Some(s) => match regex::bytes::Regex::new(&s) {
892 Ok(r) => DomainRule::Regex(r),
893 Err(_) => return Err(()),
894 },
895 None => return Err(()),
896 }
897 } else if s.contains('*') {
898 if s.starts_with('*') {
899 match ::idna::domain_to_ascii(s) {
900 Ok(r) => DomainRule::Wildcard(r),
901 Err(_) => return Err(()),
902 }
903 } else {
904 return Err(());
905 }
906 } else {
907 match ::idna::domain_to_ascii(s) {
908 Ok(r) => DomainRule::Exact(r),
909 Err(_) => return Err(()),
910 }
911 })
912 }
913}
914
915#[derive(Clone, Debug)]
916pub enum PathRule {
917 Prefix(String),
918 Regex(Regex),
919 Equals(String),
920}
921
922#[derive(PartialEq, Eq)]
923pub enum PathRuleResult {
924 Regex,
925 Prefix(usize),
926 Equals,
927 None,
928}
929
930impl PathRule {
931 pub fn matches(&self, path: &[u8]) -> PathRuleResult {
932 match self {
933 PathRule::Prefix(prefix) => {
934 if path.starts_with(prefix.as_bytes()) {
935 debug_assert!(
939 prefix.len() <= path.len(),
940 "a matching prefix cannot be longer than the path it matched",
941 );
942 PathRuleResult::Prefix(prefix.len())
943 } else {
944 PathRuleResult::None
945 }
946 }
947 PathRule::Regex(regex) => {
948 let start = Instant::now();
949 let is_a_match = regex.is_match(path);
950 let now = Instant::now();
951 time!(
952 names::event_loop::REGEX_MATCHING_TIME,
953 (now - start).as_millis()
954 );
955
956 if is_a_match {
957 PathRuleResult::Regex
958 } else {
959 PathRuleResult::None
960 }
961 }
962 PathRule::Equals(pattern) => {
963 if path == pattern.as_bytes() {
964 PathRuleResult::Equals
965 } else {
966 PathRuleResult::None
967 }
968 }
969 }
970 }
971
972 pub fn from_config(rule: CommandPathRule) -> Option<Self> {
973 match PathRuleKind::try_from(rule.kind) {
974 Ok(PathRuleKind::Prefix) => Some(PathRule::Prefix(rule.value)),
975 Ok(PathRuleKind::Regex) => Regex::new(&rule.value).ok().map(PathRule::Regex),
976 Ok(PathRuleKind::Equals) => Some(PathRule::Equals(rule.value)),
977 Err(_) => None,
978 }
979 }
980}
981
982impl std::cmp::PartialEq for PathRule {
983 fn eq(&self, other: &Self) -> bool {
984 match (self, other) {
985 (PathRule::Prefix(s1), PathRule::Prefix(s2)) => s1 == s2,
986 (PathRule::Regex(r1), PathRule::Regex(r2)) => r1.as_str() == r2.as_str(),
987 _ => false,
988 }
989 }
990}
991
992#[derive(Clone, Debug, PartialEq, Eq)]
993pub struct MethodRule {
994 pub inner: Option<Method>,
995}
996
997#[derive(PartialEq, Eq)]
998pub enum MethodRuleResult {
999 All,
1000 Equals,
1001 None,
1002}
1003
1004impl MethodRule {
1005 pub fn new(method: Option<String>) -> Self {
1006 MethodRule {
1007 inner: method.map(|s| Method::new(s.as_bytes())),
1008 }
1009 }
1010
1011 pub fn matches(&self, method: &Method) -> MethodRuleResult {
1012 match self.inner {
1013 None => MethodRuleResult::All,
1014 Some(ref m) => {
1015 if method == m {
1016 MethodRuleResult::Equals
1017 } else {
1018 MethodRuleResult::None
1019 }
1020 }
1021 }
1022 }
1023}
1024
1025#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1047pub enum Route {
1048 Deny,
1050 ClusterId(ClusterId),
1052 Frontend(Rc<Frontend>),
1057}
1058
1059fn build_listener_hsts_edit(new_hsts: Option<&HstsConfig>) -> Option<HeaderEdit> {
1086 let cfg = new_hsts?;
1087 if !matches!(cfg.enabled, Some(true)) {
1088 return None;
1089 }
1090 let rendered = render_hsts(cfg)?;
1091 let mode = if matches!(cfg.force_replace_backend, Some(true)) {
1092 HeaderEditMode::Set
1093 } else {
1094 HeaderEditMode::SetIfAbsent
1095 };
1096 Some(HeaderEdit {
1097 key: Rc::from(&b"strict-transport-security"[..]),
1098 val: rendered.into_bytes().into(),
1099 mode,
1100 })
1101}
1102
1103fn rebuild_with_listener_hsts(frontend: &Frontend, new_edit: Option<&HeaderEdit>) -> Frontend {
1119 let mut headers_response: Vec<HeaderEdit> = frontend
1121 .headers_response
1122 .iter()
1123 .filter(|edit| !edit.key.eq_ignore_ascii_case(b"strict-transport-security"))
1124 .cloned()
1125 .collect();
1126
1127 if let Some(edit) = new_edit {
1130 headers_response.push(edit.clone());
1131 }
1132
1133 Frontend {
1134 headers_response: headers_response.into(),
1135 ..frontend.clone()
1137 }
1138}
1139
1140pub fn render_hsts(cfg: &HstsConfig) -> Option<String> {
1153 let max_age = cfg.max_age?;
1154 let mut s = format!("max-age={max_age}");
1155 if matches!(cfg.include_subdomains, Some(true)) {
1156 s.push_str("; includeSubDomains");
1157 }
1158 if matches!(cfg.preload, Some(true)) {
1159 s.push_str("; preload");
1160 }
1161 Some(s)
1162}
1163
1164#[derive(Clone, PartialEq, Eq)]
1176pub struct HeaderEdit {
1177 pub key: Rc<[u8]>,
1178 pub val: Rc<[u8]>,
1179 pub mode: HeaderEditMode,
1180}
1181
1182impl Debug for HeaderEdit {
1183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1184 f.write_fmt(format_args!(
1185 "({:?}, {:?}, {:?})",
1186 String::from_utf8_lossy(&self.key),
1187 String::from_utf8_lossy(&self.val),
1188 self.mode,
1189 ))
1190 }
1191}
1192
1193#[derive(Debug, Clone, PartialEq, Eq)]
1201enum RewritePart {
1202 String(String),
1203 Host(usize),
1204 Path(usize),
1205}
1206
1207#[derive(Debug, Clone, PartialEq, Eq)]
1222pub struct RewriteParts(Vec<RewritePart>);
1223
1224impl RewriteParts {
1225 pub fn parse(
1237 template: &str,
1238 host_cap_cap: usize,
1239 path_cap_cap: usize,
1240 used_index_host: &mut usize,
1241 used_index_path: &mut usize,
1242 ) -> Option<Self> {
1243 let mut result = Vec::new();
1244 let mut i = 0;
1245 let pattern = template.as_bytes();
1246 while i < pattern.len() {
1247 if pattern[i] == b'$' {
1248 let is_host = if pattern[i..].starts_with(b"$HOST[") {
1249 i += 6;
1250 true
1251 } else if pattern[i..].starts_with(b"$PATH[") {
1252 i += 6;
1253 false
1254 } else {
1255 return None;
1256 };
1257 let mut index = 0usize;
1258 let digits_start = i;
1259 while i < pattern.len() && pattern[i].is_ascii_digit() {
1260 index = index
1261 .checked_mul(10)?
1262 .checked_add((pattern[i] - b'0') as usize)?;
1263 i += 1;
1264 }
1265 if i == digits_start {
1266 return None;
1268 }
1269 if i >= pattern.len() || pattern[i] != b']' {
1270 return None;
1271 }
1272 if is_host {
1273 if index >= host_cap_cap {
1274 return None;
1275 }
1276 if index >= *used_index_host {
1277 *used_index_host = index + 1;
1278 }
1279 result.push(RewritePart::Host(index));
1280 } else {
1281 if index >= path_cap_cap {
1282 return None;
1283 }
1284 if index >= *used_index_path {
1285 *used_index_path = index + 1;
1286 }
1287 result.push(RewritePart::Path(index));
1288 }
1289 i += 1; } else {
1291 let start = i;
1292 while i < pattern.len() && pattern[i] != b'$' {
1293 i += 1;
1294 }
1295 result.push(RewritePart::String(template[start..i].to_owned()));
1300 }
1301 }
1302 debug_assert!(
1305 result.iter().all(|part| match part {
1306 RewritePart::Host(idx) => *idx < host_cap_cap,
1307 RewritePart::Path(idx) => *idx < path_cap_cap,
1308 RewritePart::String(_) => true,
1309 }),
1310 "a parsed rewrite template must only reference captures within the rule's caps",
1311 );
1312 debug_assert!(
1313 *used_index_host <= host_cap_cap && *used_index_path <= path_cap_cap,
1314 "the highest referenced capture index cannot exceed the cap",
1315 );
1316 Some(Self(result))
1317 }
1318
1319 pub fn run(&self, host_captures: &[&str], path_captures: &[&str]) -> String {
1324 let mut cap = 0usize;
1325 for part in &self.0 {
1326 cap += match part {
1327 RewritePart::String(s) => s.len(),
1328 RewritePart::Host(i) => host_captures.get(*i).map(|s| s.len()).unwrap_or(0),
1329 RewritePart::Path(i) => path_captures.get(*i).map(|s| s.len()).unwrap_or(0),
1330 };
1331 }
1332 let mut result = String::with_capacity(cap);
1333 for part in &self.0 {
1334 let _ = match part {
1336 RewritePart::String(s) => result.write_str(s),
1337 RewritePart::Host(i) => result.write_str(host_captures.get(*i).unwrap_or(&"")),
1338 RewritePart::Path(i) => result.write_str(path_captures.get(*i).unwrap_or(&"")),
1339 };
1340 }
1341 debug_assert_eq!(
1345 result.len(),
1346 cap,
1347 "rewrite output length must equal the pre-computed one-pass capacity",
1348 );
1349 result
1350 }
1351}
1352
1353#[derive(Debug, Clone)]
1366pub struct Frontend {
1367 pub cluster_id: Option<ClusterId>,
1368 pub redirect: RedirectPolicy,
1369 pub redirect_scheme: RedirectScheme,
1370 pub redirect_template: Option<String>,
1371 pub capture_cap_host: usize,
1375 pub capture_cap_path: usize,
1379 pub rewrite_host: Option<RewriteParts>,
1380 pub rewrite_path: Option<RewriteParts>,
1381 pub rewrite_port: Option<u16>,
1382 pub headers_request: Rc<[HeaderEdit]>,
1383 pub headers_response: Rc<[HeaderEdit]>,
1384 pub required_auth: bool,
1385 pub tags: Option<Rc<CachedTags>>,
1386 pub inherits_listener_hsts: bool,
1395}
1396
1397#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1405pub enum HstsOrigin {
1406 Explicit,
1411 InheritedFromListenerDefault,
1416}
1417
1418impl PartialEq for Frontend {
1419 fn eq(&self, other: &Self) -> bool {
1420 self.cluster_id == other.cluster_id
1424 && self.redirect == other.redirect
1425 && self.redirect_scheme == other.redirect_scheme
1426 && self.redirect_template == other.redirect_template
1427 && self.rewrite_host == other.rewrite_host
1428 && self.rewrite_path == other.rewrite_path
1429 && self.rewrite_port == other.rewrite_port
1430 && self.headers_request == other.headers_request
1431 && self.headers_response == other.headers_response
1432 && self.required_auth == other.required_auth
1433 }
1434}
1435
1436impl Eq for Frontend {}
1437
1438impl std::hash::Hash for Frontend {
1439 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1440 self.cluster_id.hash(state);
1441 (self.redirect as i32).hash(state);
1444 (self.redirect_scheme as i32).hash(state);
1445 self.redirect_template.hash(state);
1446 self.required_auth.hash(state);
1447 }
1448}
1449
1450impl PartialOrd for Frontend {
1451 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1452 Some(self.cmp(other))
1453 }
1454}
1455
1456impl Ord for Frontend {
1457 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1458 self.cluster_id
1459 .cmp(&other.cluster_id)
1460 .then_with(|| (self.redirect as i32).cmp(&(other.redirect as i32)))
1461 .then_with(|| (self.redirect_scheme as i32).cmp(&(other.redirect_scheme as i32)))
1462 .then_with(|| self.redirect_template.cmp(&other.redirect_template))
1463 .then_with(|| self.required_auth.cmp(&other.required_auth))
1464 }
1465}
1466
1467impl Frontend {
1468 #[allow(clippy::too_many_arguments)]
1489 pub fn new(
1490 domain_rule: &DomainRule,
1491 path_rule: &PathRule,
1492 front: &HttpFrontend,
1493 redirect: RedirectPolicy,
1494 redirect_scheme: RedirectScheme,
1495 redirect_template: Option<String>,
1496 rewrite_host: Option<String>,
1497 rewrite_path: Option<String>,
1498 rewrite_port: Option<u16>,
1499 headers: &[sozu_command::proto::command::Header],
1500 required_auth: bool,
1501 hsts_origin: HstsOrigin,
1502 ) -> Result<Self, RouterError> {
1503 let hsts = front.hsts.as_ref();
1510 let inherits_listener_hsts =
1511 matches!(hsts_origin, HstsOrigin::InheritedFromListenerDefault) && hsts.is_some();
1512 let cluster_id = front.cluster_id.clone();
1513 let tags = front
1514 .tags
1515 .clone()
1516 .map(|tags| Rc::new(CachedTags::new(tags)));
1517
1518 let redirect_template = redirect_template.filter(|s| !s.is_empty());
1523 let rewrite_host = rewrite_host.filter(|s| !s.is_empty());
1524 let rewrite_path = rewrite_path.filter(|s| !s.is_empty());
1525
1526 let deny = match (&cluster_id, redirect) {
1527 (_, RedirectPolicy::Unauthorized) => true,
1528 (None, RedirectPolicy::Forward) => {
1529 warn!(
1530 "{} Frontend[domain: {:?}, path: {:?}]: forward on clusterless frontends are unauthorized",
1531 log_module_context!(),
1532 domain_rule,
1533 path_rule,
1534 );
1535 true
1536 }
1537 _ => false,
1538 };
1539 if deny {
1540 let mut deny_headers_response: Vec<HeaderEdit> = Vec::new();
1548 if let Some(cfg) = hsts
1549 && matches!(cfg.enabled, Some(true))
1550 && let Some(rendered) = render_hsts(cfg)
1551 {
1552 let mode = if matches!(cfg.force_replace_backend, Some(true)) {
1553 HeaderEditMode::Set
1554 } else {
1555 HeaderEditMode::SetIfAbsent
1556 };
1557 deny_headers_response.push(HeaderEdit {
1558 key: Rc::from(&b"strict-transport-security"[..]),
1559 val: rendered.into_bytes().into(),
1560 mode,
1561 });
1562 crate::incr!(names::http::HSTS_FRONTEND_ADDED);
1563 }
1564
1565 return Ok(Self {
1566 cluster_id,
1567 redirect: RedirectPolicy::Unauthorized,
1568 redirect_scheme,
1569 redirect_template: None,
1570 capture_cap_host: 0,
1571 capture_cap_path: 0,
1572 rewrite_host: None,
1573 rewrite_path: None,
1574 rewrite_port: None,
1575 headers_request: Rc::new([]),
1576 headers_response: deny_headers_response.into(),
1577 required_auth,
1578 tags,
1579 inherits_listener_hsts,
1580 });
1581 }
1582
1583 let mut capture_cap_host = match domain_rule {
1588 DomainRule::Any => 1,
1589 DomainRule::Exact(_) => 1,
1590 DomainRule::Wildcard(_) => 2,
1591 DomainRule::Regex(regex) => regex.captures_len(),
1592 };
1593 let mut capture_cap_path = match path_rule {
1594 PathRule::Equals(_) => 1,
1595 PathRule::Prefix(_) => 2,
1596 PathRule::Regex(regex) => regex.captures_len(),
1597 };
1598 let mut used_capture_host = 0usize;
1599 let mut used_capture_path = 0usize;
1600 let rewrite_host_parts = if let Some(p) = rewrite_host {
1601 Some(
1602 RewriteParts::parse(
1603 &p,
1604 capture_cap_host,
1605 capture_cap_path,
1606 &mut used_capture_host,
1607 &mut used_capture_path,
1608 )
1609 .ok_or(RouterError::InvalidHostRewrite(p))?,
1610 )
1611 } else {
1612 None
1613 };
1614 let rewrite_path_parts = if let Some(p) = rewrite_path {
1615 Some(
1616 RewriteParts::parse(
1617 &p,
1618 capture_cap_host,
1619 capture_cap_path,
1620 &mut used_capture_host,
1621 &mut used_capture_path,
1622 )
1623 .ok_or(RouterError::InvalidPathRewrite(p))?,
1624 )
1625 } else {
1626 None
1627 };
1628 if used_capture_host == 0 {
1631 capture_cap_host = 0;
1632 }
1633 if used_capture_path == 0 {
1634 capture_cap_path = 0;
1635 }
1636
1637 let mut headers_request = Vec::new();
1638 let mut headers_response = Vec::new();
1639 for header in headers {
1640 let edit = HeaderEdit {
1641 key: header.key.as_bytes().into(),
1642 val: header.val.as_bytes().into(),
1643 mode: HeaderEditMode::Append,
1644 };
1645 match header.position() {
1646 HeaderPosition::Request => headers_request.push(edit),
1647 HeaderPosition::Response => headers_response.push(edit),
1648 HeaderPosition::Both => {
1649 headers_request.push(edit.clone());
1650 headers_response.push(edit);
1651 }
1652 HeaderPosition::Unspecified => {
1658 warn!(
1659 "{} dropping Header {{ key: {:?}, val: {:?} }} with HEADER_POSITION_UNSPECIFIED",
1660 log_module_context!(),
1661 header.key,
1662 header.val,
1663 );
1664 }
1665 }
1666 }
1667
1668 if let Some(cfg) = hsts
1676 && matches!(cfg.enabled, Some(true))
1677 {
1678 if let Some(rendered) = render_hsts(cfg) {
1679 let mode = if matches!(cfg.force_replace_backend, Some(true)) {
1685 HeaderEditMode::Set
1686 } else {
1687 HeaderEditMode::SetIfAbsent
1688 };
1689 headers_response.push(HeaderEdit {
1690 key: Rc::from(&b"strict-transport-security"[..]),
1691 val: rendered.into_bytes().into(),
1692 mode,
1693 });
1694 crate::incr!(names::http::HSTS_FRONTEND_ADDED);
1695 } else {
1696 warn!(
1704 "{} HSTS enabled = true on frontend {:?} but render_hsts \
1705 returned None (max_age missing). Frontend will not emit \
1706 Strict-Transport-Security; the config layer that built \
1707 this HstsConfig must substitute DEFAULT_HSTS_MAX_AGE.",
1708 log_module_context!(),
1709 cluster_id,
1710 );
1711 crate::incr!(names::http::HSTS_UNRENDERED);
1712 }
1713 }
1714
1715 Ok(Frontend {
1716 cluster_id,
1717 redirect,
1718 redirect_scheme,
1719 redirect_template,
1720 capture_cap_host,
1721 capture_cap_path,
1722 rewrite_host: rewrite_host_parts,
1723 rewrite_path: rewrite_path_parts,
1724 rewrite_port,
1725 headers_request: headers_request.into(),
1726 headers_response: headers_response.into(),
1727 required_auth,
1728 tags,
1729 inherits_listener_hsts,
1730 })
1731 }
1732
1733 pub(crate) fn minimal_forward(cluster_id: ClusterId) -> Self {
1744 Self {
1745 cluster_id: Some(cluster_id),
1746 redirect: RedirectPolicy::Forward,
1747 redirect_scheme: RedirectScheme::UseSame,
1748 redirect_template: None,
1749 capture_cap_host: 0,
1750 capture_cap_path: 0,
1751 rewrite_host: None,
1752 rewrite_path: None,
1753 rewrite_port: None,
1754 headers_request: Rc::new([]),
1755 headers_response: Rc::new([]),
1756 required_auth: false,
1757 tags: None,
1758 inherits_listener_hsts: true,
1759 }
1760 }
1761
1762 pub(crate) fn minimal_deny() -> Self {
1773 Self {
1774 cluster_id: None,
1775 redirect: RedirectPolicy::Unauthorized,
1776 redirect_scheme: RedirectScheme::UseSame,
1777 redirect_template: None,
1778 capture_cap_host: 0,
1779 capture_cap_path: 0,
1780 rewrite_host: None,
1781 rewrite_path: None,
1782 rewrite_port: None,
1783 headers_request: Rc::new([]),
1784 headers_response: Rc::new([]),
1785 required_auth: false,
1786 tags: None,
1787 inherits_listener_hsts: true,
1788 }
1789 }
1790}
1791
1792#[derive(Debug, Clone, PartialEq)]
1807pub struct RouteResult {
1808 pub cluster_id: Option<ClusterId>,
1809 pub redirect: RedirectPolicy,
1810 pub redirect_scheme: RedirectScheme,
1811 pub redirect_template: Option<String>,
1812 pub rewritten_host: Option<String>,
1813 pub rewritten_path: Option<String>,
1814 pub rewritten_port: Option<u16>,
1815 pub headers_request: Rc<[HeaderEdit]>,
1816 pub headers_response: Rc<[HeaderEdit]>,
1817 pub required_auth: bool,
1818 pub tags: Option<Rc<CachedTags>>,
1819}
1820
1821impl RouteResult {
1822 pub fn deny(cluster_id: Option<ClusterId>) -> Self {
1824 Self {
1825 cluster_id,
1826 redirect: RedirectPolicy::Unauthorized,
1827 redirect_scheme: RedirectScheme::UseSame,
1828 redirect_template: None,
1829 rewritten_host: None,
1830 rewritten_path: None,
1831 rewritten_port: None,
1832 headers_request: Rc::new([]),
1833 headers_response: Rc::new([]),
1834 required_auth: false,
1835 tags: None,
1836 }
1837 }
1838
1839 pub fn forward(cluster_id: ClusterId) -> Self {
1842 Self {
1843 cluster_id: Some(cluster_id),
1844 redirect: RedirectPolicy::Forward,
1845 redirect_scheme: RedirectScheme::UseSame,
1846 redirect_template: None,
1847 rewritten_host: None,
1848 rewritten_path: None,
1849 rewritten_port: None,
1850 headers_request: Rc::new([]),
1851 headers_response: Rc::new([]),
1852 required_auth: false,
1853 tags: None,
1854 }
1855 }
1856
1857 fn from_frontend(
1860 frontend: &Frontend,
1861 captures_host: Vec<&str>,
1862 path: &[u8],
1863 path_rule: &PathRule,
1864 ) -> Self {
1865 if frontend.redirect == RedirectPolicy::Unauthorized {
1873 return Self {
1874 cluster_id: frontend.cluster_id.clone(),
1875 redirect: RedirectPolicy::Unauthorized,
1876 redirect_scheme: frontend.redirect_scheme,
1877 redirect_template: frontend.redirect_template.clone(),
1878 rewritten_host: None,
1879 rewritten_path: None,
1880 rewritten_port: None,
1881 headers_request: Rc::new([]),
1882 headers_response: frontend.headers_response.clone(),
1883 required_auth: frontend.required_auth,
1884 tags: frontend.tags.clone(),
1885 };
1886 }
1887
1888 let mut captures_path: Vec<&str> = Vec::with_capacity(frontend.capture_cap_path);
1889 if frontend.capture_cap_path > 0 {
1890 captures_path.push(from_utf8(path).unwrap_or_default());
1891 match path_rule {
1892 PathRule::Prefix(prefix) => {
1893 let tail_start = prefix.len().min(path.len());
1894 captures_path.push(from_utf8(&path[tail_start..]).unwrap_or_default());
1895 }
1896 PathRule::Regex(regex) => {
1897 if let Some(caps) = regex.captures(path) {
1898 captures_path.extend(caps.iter().skip(1).map(|c| {
1899 c.map(|m| from_utf8(m.as_bytes()).unwrap_or_default())
1900 .unwrap_or("")
1901 }));
1902 }
1903 }
1904 PathRule::Equals(_) => {}
1905 }
1906 }
1907
1908 Self {
1909 cluster_id: frontend.cluster_id.clone(),
1910 redirect: frontend.redirect,
1911 redirect_scheme: frontend.redirect_scheme,
1912 redirect_template: frontend.redirect_template.clone(),
1913 rewritten_host: frontend
1914 .rewrite_host
1915 .as_ref()
1916 .map(|rewrite| rewrite.run(&captures_host, &captures_path)),
1917 rewritten_path: frontend
1918 .rewrite_path
1919 .as_ref()
1920 .map(|rewrite| rewrite.run(&captures_host, &captures_path)),
1921 rewritten_port: frontend.rewrite_port,
1922 headers_request: frontend.headers_request.clone(),
1923 headers_response: frontend.headers_response.clone(),
1924 required_auth: frontend.required_auth,
1925 tags: frontend.tags.clone(),
1926 }
1927 }
1928
1929 fn new_no_trie<'a>(
1934 domain: &'a [u8],
1935 domain_rule: &DomainRule,
1936 path: &'a [u8],
1937 path_rule: &PathRule,
1938 route: &Route,
1939 ) -> Self {
1940 let frontend = match route {
1941 Route::Frontend(f) => f.clone(),
1942 Route::ClusterId(id) => return Self::forward(id.clone()),
1943 Route::Deny => return Self::deny(None),
1944 };
1945 let mut captures_host: Vec<&str> = Vec::with_capacity(frontend.capture_cap_host);
1946 if frontend.capture_cap_host > 0 {
1947 captures_host.push(from_utf8(domain).unwrap_or_default());
1948 match domain_rule {
1949 DomainRule::Wildcard(suffix) => {
1950 let head_end = domain.len().saturating_sub(suffix.len().saturating_sub(1));
1951 captures_host.push(from_utf8(&domain[..head_end]).unwrap_or_default());
1952 }
1953 DomainRule::Regex(regex) => {
1954 if let Some(caps) = regex.captures(domain) {
1955 captures_host.extend(caps.iter().skip(1).map(|c| {
1956 c.map(|m| from_utf8(m.as_bytes()).unwrap_or_default())
1957 .unwrap_or("")
1958 }));
1959 }
1960 }
1961 DomainRule::Any | DomainRule::Exact(_) => {}
1962 }
1963 }
1964 Self::from_frontend(&frontend, captures_host, path, path_rule)
1965 }
1966
1967 fn new_with_trie<'a, 'b>(
1972 domain: &'a [u8],
1973 domain_submatches: TrieMatches<'a, 'b>,
1974 path: &'a [u8],
1975 path_rule: &PathRule,
1976 route: &Route,
1977 ) -> Self {
1978 let frontend = match route {
1979 Route::Frontend(f) => f.clone(),
1980 Route::ClusterId(id) => return Self::forward(id.clone()),
1981 Route::Deny => return Self::deny(None),
1982 };
1983 let mut captures_host: Vec<&str> = Vec::with_capacity(frontend.capture_cap_host);
1984 if frontend.capture_cap_host > 0 {
1985 captures_host.push(from_utf8(domain).unwrap_or_default());
1986 for submatch in &domain_submatches {
1987 match submatch {
1988 TrieSubMatch::Wildcard(part) => {
1989 captures_host.push(from_utf8(part).unwrap_or_default());
1990 }
1991 TrieSubMatch::Regexp(part, regex) => {
1992 if let Some(caps) = regex.captures(part) {
1993 captures_host.extend(caps.iter().skip(1).map(|c| {
1994 c.map(|m| from_utf8(m.as_bytes()).unwrap_or_default())
1995 .unwrap_or("")
1996 }));
1997 }
1998 }
1999 }
2000 }
2001 }
2002 Self::from_frontend(&frontend, captures_host, path, path_rule)
2003 }
2004}
2005
2006#[cfg(test)]
2007mod tests {
2008 use super::*;
2009
2010 #[test]
2011 fn render_hsts_max_age_only() {
2012 let cfg = HstsConfig {
2013 enabled: Some(true),
2014 max_age: Some(31_536_000),
2015 include_subdomains: None,
2016 preload: None,
2017 force_replace_backend: None,
2018 };
2019 assert_eq!(render_hsts(&cfg), Some("max-age=31536000".to_owned()));
2020 }
2021
2022 #[test]
2023 fn render_hsts_with_include_subdomains() {
2024 let cfg = HstsConfig {
2025 enabled: Some(true),
2026 max_age: Some(31_536_000),
2027 include_subdomains: Some(true),
2028 preload: None,
2029 force_replace_backend: None,
2030 };
2031 assert_eq!(
2032 render_hsts(&cfg),
2033 Some("max-age=31536000; includeSubDomains".to_owned())
2034 );
2035 }
2036
2037 #[test]
2038 fn render_hsts_with_preload_only() {
2039 let cfg = HstsConfig {
2040 enabled: Some(true),
2041 max_age: Some(63_072_000),
2042 include_subdomains: None,
2043 preload: Some(true),
2044 force_replace_backend: None,
2045 };
2046 assert_eq!(
2047 render_hsts(&cfg),
2048 Some("max-age=63072000; preload".to_owned())
2049 );
2050 }
2051
2052 #[test]
2053 fn render_hsts_full() {
2054 let cfg = HstsConfig {
2055 enabled: Some(true),
2056 max_age: Some(31_536_000),
2057 include_subdomains: Some(true),
2058 preload: Some(true),
2059 force_replace_backend: None,
2060 };
2061 assert_eq!(
2062 render_hsts(&cfg),
2063 Some("max-age=31536000; includeSubDomains; preload".to_owned())
2064 );
2065 }
2066
2067 #[test]
2068 fn render_hsts_kill_switch_max_age_zero() {
2069 let cfg = HstsConfig {
2070 enabled: Some(true),
2071 max_age: Some(0),
2072 include_subdomains: Some(true),
2073 preload: None,
2074 force_replace_backend: None,
2075 };
2076 assert_eq!(
2080 render_hsts(&cfg),
2081 Some("max-age=0; includeSubDomains".to_owned())
2082 );
2083 }
2084
2085 #[test]
2086 fn render_hsts_omitted_when_max_age_missing() {
2087 let cfg = HstsConfig {
2088 enabled: Some(true),
2089 max_age: None,
2090 include_subdomains: Some(true),
2091 preload: None,
2092 force_replace_backend: None,
2093 };
2094 assert_eq!(render_hsts(&cfg), None);
2098 }
2099
2100 #[test]
2101 fn rebuild_with_listener_hsts_replaces_existing_entry() {
2102 let frontend = Frontend {
2106 cluster_id: Some("api".to_owned()),
2107 redirect: RedirectPolicy::Forward,
2108 redirect_scheme: RedirectScheme::UseSame,
2109 redirect_template: None,
2110 capture_cap_host: 0,
2111 capture_cap_path: 0,
2112 rewrite_host: None,
2113 rewrite_path: None,
2114 rewrite_port: None,
2115 headers_request: Rc::new([]),
2116 headers_response: Rc::from(vec![
2117 HeaderEdit {
2118 key: Rc::from(&b"x-cache"[..]),
2119 val: Rc::from(&b"hit"[..]),
2120 mode: HeaderEditMode::Append,
2121 },
2122 HeaderEdit {
2123 key: Rc::from(&b"strict-transport-security"[..]),
2124 val: Rc::from(&b"max-age=31536000"[..]),
2125 mode: HeaderEditMode::SetIfAbsent,
2126 },
2127 ]),
2128 required_auth: false,
2129 tags: None,
2130 inherits_listener_hsts: true,
2131 };
2132 let new_hsts = HstsConfig {
2133 enabled: Some(true),
2134 max_age: Some(63_072_000),
2135 include_subdomains: Some(true),
2136 preload: None,
2137 force_replace_backend: None,
2138 };
2139 let new_edit = build_listener_hsts_edit(Some(&new_hsts));
2140 let rebuilt = rebuild_with_listener_hsts(&frontend, new_edit.as_ref());
2141
2142 let response: Vec<_> = rebuilt.headers_response.iter().collect();
2143 assert_eq!(response.len(), 2, "x-cache + new STS, no leftover STS");
2144 assert_eq!(&*response[0].key, b"x-cache");
2145 assert_eq!(&*response[1].key, b"strict-transport-security");
2146 assert_eq!(
2147 &*response[1].val,
2148 b"max-age=63072000; includeSubDomains".as_slice()
2149 );
2150 assert!(rebuilt.inherits_listener_hsts);
2151 }
2152
2153 #[test]
2154 fn rebuild_with_listener_hsts_strips_when_none() {
2155 let frontend = Frontend {
2158 cluster_id: Some("api".to_owned()),
2159 redirect: RedirectPolicy::Forward,
2160 redirect_scheme: RedirectScheme::UseSame,
2161 redirect_template: None,
2162 capture_cap_host: 0,
2163 capture_cap_path: 0,
2164 rewrite_host: None,
2165 rewrite_path: None,
2166 rewrite_port: None,
2167 headers_request: Rc::new([]),
2168 headers_response: Rc::from(vec![
2169 HeaderEdit {
2170 key: Rc::from(&b"x-cache"[..]),
2171 val: Rc::from(&b"hit"[..]),
2172 mode: HeaderEditMode::Append,
2173 },
2174 HeaderEdit {
2175 key: Rc::from(&b"strict-transport-security"[..]),
2176 val: Rc::from(&b"max-age=31536000"[..]),
2177 mode: HeaderEditMode::SetIfAbsent,
2178 },
2179 ]),
2180 required_auth: false,
2181 tags: None,
2182 inherits_listener_hsts: true,
2183 };
2184 let new_edit = build_listener_hsts_edit(None);
2185 let rebuilt = rebuild_with_listener_hsts(&frontend, new_edit.as_ref());
2186 let response: Vec<_> = rebuilt.headers_response.iter().collect();
2187 assert_eq!(response.len(), 1);
2188 assert_eq!(&*response[0].key, b"x-cache");
2189 }
2190
2191 #[test]
2192 fn rebuild_with_listener_hsts_disabled_strips() {
2193 let frontend = Frontend {
2196 cluster_id: Some("api".to_owned()),
2197 redirect: RedirectPolicy::Forward,
2198 redirect_scheme: RedirectScheme::UseSame,
2199 redirect_template: None,
2200 capture_cap_host: 0,
2201 capture_cap_path: 0,
2202 rewrite_host: None,
2203 rewrite_path: None,
2204 rewrite_port: None,
2205 headers_request: Rc::new([]),
2206 headers_response: Rc::from(vec![HeaderEdit {
2207 key: Rc::from(&b"strict-transport-security"[..]),
2208 val: Rc::from(&b"max-age=31536000"[..]),
2209 mode: HeaderEditMode::SetIfAbsent,
2210 }]),
2211 required_auth: false,
2212 tags: None,
2213 inherits_listener_hsts: true,
2214 };
2215 let new_hsts = HstsConfig {
2216 enabled: Some(false),
2217 max_age: None,
2218 include_subdomains: None,
2219 preload: None,
2220 force_replace_backend: None,
2221 };
2222 let new_edit = build_listener_hsts_edit(Some(&new_hsts));
2223 let rebuilt = rebuild_with_listener_hsts(&frontend, new_edit.as_ref());
2224 assert_eq!(rebuilt.headers_response.len(), 0);
2225 }
2226
2227 #[test]
2228 fn refresh_inheriting_hsts_skips_explicit_overrides() {
2229 use crate::router::pattern_trie::TrieNode;
2233 let mut router = Router {
2234 pre: Vec::new(),
2235 tree: TrieNode::root(),
2236 post: Vec::new(),
2237 };
2238 let inheriting = Frontend {
2239 cluster_id: Some("api".to_owned()),
2240 redirect: RedirectPolicy::Forward,
2241 redirect_scheme: RedirectScheme::UseSame,
2242 redirect_template: None,
2243 capture_cap_host: 0,
2244 capture_cap_path: 0,
2245 rewrite_host: None,
2246 rewrite_path: None,
2247 rewrite_port: None,
2248 headers_request: Rc::new([]),
2249 headers_response: Rc::from(vec![HeaderEdit {
2250 key: Rc::from(&b"strict-transport-security"[..]),
2251 val: Rc::from(&b"max-age=31536000"[..]),
2252 mode: HeaderEditMode::SetIfAbsent,
2253 }]),
2254 required_auth: false,
2255 tags: None,
2256 inherits_listener_hsts: true,
2257 };
2258 let explicit = Frontend {
2259 cluster_id: Some("legacy".to_owned()),
2260 redirect: RedirectPolicy::Forward,
2261 redirect_scheme: RedirectScheme::UseSame,
2262 redirect_template: None,
2263 capture_cap_host: 0,
2264 capture_cap_path: 0,
2265 rewrite_host: None,
2266 rewrite_path: None,
2267 rewrite_port: None,
2268 headers_request: Rc::new([]),
2269 headers_response: Rc::from(vec![HeaderEdit {
2270 key: Rc::from(&b"strict-transport-security"[..]),
2271 val: Rc::from(&b"max-age=300"[..]),
2272 mode: HeaderEditMode::SetIfAbsent,
2273 }]),
2274 required_auth: false,
2275 tags: None,
2276 inherits_listener_hsts: false,
2277 };
2278 router.pre.push((
2279 DomainRule::Any,
2280 PathRule::Prefix("/api".to_owned()),
2281 MethodRule::new(None),
2282 Route::Frontend(Rc::new(inheriting)),
2283 ));
2284 router.post.push((
2285 DomainRule::Any,
2286 PathRule::Prefix("/legacy".to_owned()),
2287 MethodRule::new(None),
2288 Route::Frontend(Rc::new(explicit)),
2289 ));
2290
2291 let new_hsts = HstsConfig {
2292 enabled: Some(true),
2293 max_age: Some(63_072_000),
2294 include_subdomains: Some(true),
2295 preload: None,
2296 force_replace_backend: None,
2297 };
2298 let count = router.refresh_inheriting_hsts(Some(&new_hsts));
2299 assert_eq!(count, 1, "only the inheriting frontend should refresh");
2300
2301 if let Route::Frontend(rc) = &router.pre[0].3 {
2302 let response: Vec<_> = rc.headers_response.iter().collect();
2303 assert_eq!(
2304 &*response.last().unwrap().val,
2305 b"max-age=63072000; includeSubDomains".as_slice(),
2306 "inheriting frontend's STS must reflect the new listener default"
2307 );
2308 } else {
2309 panic!("pre[0] should be Route::Frontend");
2310 }
2311 if let Route::Frontend(rc) = &router.post[0].3 {
2312 let response: Vec<_> = rc.headers_response.iter().collect();
2313 assert_eq!(
2314 &*response.last().unwrap().val,
2315 b"max-age=300".as_slice(),
2316 "explicit override must be preserved unchanged"
2317 );
2318 } else {
2319 panic!("post[0] should be Route::Frontend");
2320 }
2321 }
2322
2323 #[test]
2324 fn refresh_inheriting_hsts_promotes_clusterid_on_enable() {
2325 use crate::router::pattern_trie::TrieNode;
2335 let mut router = Router {
2336 pre: Vec::new(),
2337 tree: TrieNode::root(),
2338 post: vec![(
2339 DomainRule::Any,
2340 PathRule::Prefix("/".to_owned()),
2341 MethodRule::new(None),
2342 Route::ClusterId("api".to_owned()),
2343 )],
2344 };
2345
2346 let new_hsts = HstsConfig {
2347 enabled: Some(true),
2348 max_age: Some(31_536_000),
2349 include_subdomains: Some(true),
2350 preload: None,
2351 force_replace_backend: None,
2352 };
2353 let count = router.refresh_inheriting_hsts(Some(&new_hsts));
2354 assert_eq!(count, 1, "the ClusterId entry must be promoted + counted");
2355
2356 let Route::Frontend(rc) = &router.post[0].3 else {
2357 panic!("post[0] should now be Route::Frontend, not the original Route::ClusterId");
2358 };
2359 assert_eq!(rc.cluster_id.as_deref(), Some("api"));
2360 assert_eq!(
2361 rc.redirect,
2362 RedirectPolicy::Forward,
2363 "promoted entry must keep Forward semantics so lookup yields the same backend"
2364 );
2365 assert!(
2366 rc.inherits_listener_hsts,
2367 "promoted entry must mark itself inheriting so the next patch refreshes it"
2368 );
2369 let response: Vec<_> = rc.headers_response.iter().collect();
2370 assert_eq!(
2371 response.len(),
2372 1,
2373 "promoted entry carries exactly one STS edit, no operator headers"
2374 );
2375 assert_eq!(&*response[0].key, b"strict-transport-security");
2376 assert_eq!(
2377 &*response[0].val,
2378 b"max-age=31536000; includeSubDomains".as_slice()
2379 );
2380 }
2381
2382 #[test]
2383 fn refresh_inheriting_hsts_promotes_deny_on_enable() {
2384 use crate::router::pattern_trie::TrieNode;
2390 let mut router = Router {
2391 pre: Vec::new(),
2392 tree: TrieNode::root(),
2393 post: vec![(
2394 DomainRule::Any,
2395 PathRule::Prefix("/forbidden".to_owned()),
2396 MethodRule::new(None),
2397 Route::Deny,
2398 )],
2399 };
2400
2401 let new_hsts = HstsConfig {
2402 enabled: Some(true),
2403 max_age: Some(31_536_000),
2404 include_subdomains: None,
2405 preload: None,
2406 force_replace_backend: None,
2407 };
2408 let count = router.refresh_inheriting_hsts(Some(&new_hsts));
2409 assert_eq!(count, 1);
2410
2411 let Route::Frontend(rc) = &router.post[0].3 else {
2412 panic!("post[0] should now be Route::Frontend, not the original Route::Deny");
2413 };
2414 assert_eq!(rc.cluster_id, None, "promoted Deny stays clusterless");
2415 assert_eq!(
2416 rc.redirect,
2417 RedirectPolicy::Unauthorized,
2418 "promoted Deny must keep Unauthorized so lookup yields a 401"
2419 );
2420 assert!(rc.inherits_listener_hsts);
2421 let response: Vec<_> = rc.headers_response.iter().collect();
2422 assert_eq!(response.len(), 1);
2423 assert_eq!(&*response[0].key, b"strict-transport-security");
2424 assert_eq!(&*response[0].val, b"max-age=31536000".as_slice());
2425 }
2426
2427 #[test]
2428 fn refresh_inheriting_hsts_skips_lightweight_on_disable() {
2429 use crate::router::pattern_trie::TrieNode;
2439 let make_router = || Router {
2440 pre: vec![(
2441 DomainRule::Any,
2442 PathRule::Prefix("/".to_owned()),
2443 MethodRule::new(None),
2444 Route::ClusterId("api".to_owned()),
2445 )],
2446 tree: TrieNode::root(),
2447 post: vec![(
2448 DomainRule::Any,
2449 PathRule::Prefix("/forbidden".to_owned()),
2450 MethodRule::new(None),
2451 Route::Deny,
2452 )],
2453 };
2454
2455 for (label, hsts) in [
2456 ("none", None),
2457 (
2458 "disabled",
2459 Some(HstsConfig {
2460 enabled: Some(false),
2461 max_age: None,
2462 include_subdomains: None,
2463 preload: None,
2464 force_replace_backend: None,
2465 }),
2466 ),
2467 (
2468 "enabled-without-max-age",
2469 Some(HstsConfig {
2470 enabled: Some(true),
2471 max_age: None,
2472 include_subdomains: None,
2473 preload: None,
2474 force_replace_backend: None,
2475 }),
2476 ),
2477 ] {
2478 let mut router = make_router();
2479 let count = router.refresh_inheriting_hsts(hsts.as_ref());
2480 assert_eq!(count, 0, "no promotion expected for {label}");
2481 assert!(
2482 matches!(router.pre[0].3, Route::ClusterId(_)),
2483 "{label}: ClusterId must stay lightweight"
2484 );
2485 assert!(
2486 matches!(router.post[0].3, Route::Deny),
2487 "{label}: Deny must stay lightweight"
2488 );
2489 }
2490 }
2491
2492 #[test]
2493 fn refresh_inheriting_hsts_promoted_entry_refreshes_on_subsequent_patches() {
2494 use crate::router::pattern_trie::TrieNode;
2499 let mut router = Router {
2500 pre: Vec::new(),
2501 tree: TrieNode::root(),
2502 post: vec![(
2503 DomainRule::Any,
2504 PathRule::Prefix("/".to_owned()),
2505 MethodRule::new(None),
2506 Route::ClusterId("api".to_owned()),
2507 )],
2508 };
2509
2510 let first_patch = HstsConfig {
2511 enabled: Some(true),
2512 max_age: Some(31_536_000),
2513 include_subdomains: None,
2514 preload: None,
2515 force_replace_backend: None,
2516 };
2517 assert_eq!(router.refresh_inheriting_hsts(Some(&first_patch)), 1);
2518
2519 let second_patch = HstsConfig {
2520 enabled: Some(true),
2521 max_age: Some(63_072_000),
2522 include_subdomains: Some(true),
2523 preload: None,
2524 force_replace_backend: None,
2525 };
2526 assert_eq!(
2527 router.refresh_inheriting_hsts(Some(&second_patch)),
2528 1,
2529 "the previously promoted entry must be re-counted via the path-1 branch"
2530 );
2531
2532 let Route::Frontend(rc) = &router.post[0].3 else {
2533 panic!("post[0] should still be Route::Frontend after the second patch");
2534 };
2535 let response: Vec<_> = rc.headers_response.iter().collect();
2536 assert_eq!(
2537 response.len(),
2538 1,
2539 "second patch must REPLACE the existing STS edit, not append a duplicate"
2540 );
2541 assert_eq!(
2542 &*response[0].val,
2543 b"max-age=63072000; includeSubDomains".as_slice()
2544 );
2545 }
2546
2547 #[test]
2548 fn refresh_inheriting_hsts_promoted_entry_loses_hsts_on_disable_patch() {
2549 use crate::router::pattern_trie::TrieNode;
2556 let mut router = Router {
2557 pre: vec![(
2558 DomainRule::Any,
2559 PathRule::Prefix("/".to_owned()),
2560 MethodRule::new(None),
2561 Route::ClusterId("api".to_owned()),
2562 )],
2563 tree: TrieNode::root(),
2564 post: Vec::new(),
2565 };
2566
2567 let enable = HstsConfig {
2568 enabled: Some(true),
2569 max_age: Some(31_536_000),
2570 include_subdomains: None,
2571 preload: None,
2572 force_replace_backend: None,
2573 };
2574 assert_eq!(router.refresh_inheriting_hsts(Some(&enable)), 1);
2575
2576 let disable = HstsConfig {
2577 enabled: Some(false),
2578 max_age: None,
2579 include_subdomains: None,
2580 preload: None,
2581 force_replace_backend: None,
2582 };
2583 assert_eq!(
2584 router.refresh_inheriting_hsts(Some(&disable)),
2585 1,
2586 "the promoted entry must still be touched on disable to strip its STS edit"
2587 );
2588
2589 let Route::Frontend(rc) = &router.pre[0].3 else {
2590 panic!("pre[0] should still be Route::Frontend (no demotion)");
2591 };
2592 assert_eq!(rc.cluster_id.as_deref(), Some("api"));
2593 assert_eq!(
2594 rc.headers_response.len(),
2595 0,
2596 "disable patch must strip the STS edit from the promoted entry"
2597 );
2598 }
2599
2600 #[test]
2601 fn refresh_inheriting_hsts_promotes_clusterid_in_trie_on_enable() {
2602 use crate::router::pattern_trie::TrieNode;
2609 let mut router = Router {
2610 pre: Vec::new(),
2611 tree: TrieNode::root(),
2612 post: Vec::new(),
2613 };
2614 let path_rule = PathRule::Prefix("/".to_owned());
2615 let method_rule = MethodRule::new(None);
2616 assert!(router.add_tree_rule(
2617 b"example.com",
2618 &path_rule,
2619 &method_rule,
2620 &Route::ClusterId("api".to_owned()),
2621 ));
2622
2623 let new_hsts = HstsConfig {
2624 enabled: Some(true),
2625 max_age: Some(31_536_000),
2626 include_subdomains: Some(true),
2627 preload: None,
2628 force_replace_backend: None,
2629 };
2630 let count = router.refresh_inheriting_hsts(Some(&new_hsts));
2631 assert_eq!(
2632 count, 1,
2633 "trie-resident ClusterId must be promoted + counted"
2634 );
2635
2636 let (_, paths) = router
2637 .tree
2638 .domain_lookup_mut(b"example.com", false)
2639 .expect("trie leaf still present after refresh");
2640 assert_eq!(paths.len(), 1);
2641 let Route::Frontend(rc) = &paths[0].2 else {
2642 panic!("trie leaf should now be Route::Frontend, not Route::ClusterId");
2643 };
2644 assert_eq!(rc.cluster_id.as_deref(), Some("api"));
2645 assert_eq!(rc.redirect, RedirectPolicy::Forward);
2646 assert!(rc.inherits_listener_hsts);
2647 let response: Vec<_> = rc.headers_response.iter().collect();
2648 assert_eq!(response.len(), 1);
2649 assert_eq!(&*response[0].key, b"strict-transport-security");
2650 assert_eq!(
2651 &*response[0].val,
2652 b"max-age=31536000; includeSubDomains".as_slice()
2653 );
2654 }
2655
2656 #[test]
2657 fn convert_regex() {
2658 assert_eq!(
2661 convert_regex_domain_rule("www.example.com")
2662 .unwrap()
2663 .as_str(),
2664 "\\Awww\\.example\\.com\\z"
2665 );
2666 assert_eq!(
2667 convert_regex_domain_rule("*.example.com").unwrap().as_str(),
2668 "\\A*\\.example\\.com\\z"
2669 );
2670 assert_eq!(
2671 convert_regex_domain_rule("test.*.example.com")
2672 .unwrap()
2673 .as_str(),
2674 "\\Atest\\.*\\.example\\.com\\z"
2675 );
2676 assert_eq!(
2677 convert_regex_domain_rule("css./cdn[a-z0-9]+/.example.com")
2678 .unwrap()
2679 .as_str(),
2680 "\\Acss\\.cdn[a-z0-9]+\\.example\\.com\\z"
2681 );
2682
2683 assert_eq!(
2684 convert_regex_domain_rule("css./cdn[a-z0-9]+.example.com"),
2685 None
2686 );
2687 assert_eq!(
2688 convert_regex_domain_rule("css./cdn[a-z0-9]+/a.example.com"),
2689 None
2690 );
2691 }
2692
2693 #[test]
2698 fn regex_domain_rule_rejects_suffix_and_prefix() {
2699 let rule: DomainRule = "/example\\.com/".parse().unwrap();
2700 assert!(rule.matches(b"example.com"));
2701 assert!(!rule.matches(b"attacker.example.com"));
2702 assert!(!rule.matches(b"example.com.evil.org"));
2703 assert!(!rule.matches(b"prefixexample.com"));
2704 assert!(!rule.matches(b"example.commercial"));
2705 }
2706
2707 #[test]
2713 fn regex_domain_rule_multi_segment_segments_are_isolated() {
2714 let pattern = convert_regex_domain_rule("/seg1/.foo./seg2/.com")
2715 .expect("multi-segment regex hostname must compile");
2716 assert_eq!(pattern.as_str(), "\\Aseg1\\.foo\\.seg2\\.com\\z");
2717 }
2718
2719 #[test]
2720 fn parse_domain_rule() {
2721 assert_eq!("*".parse::<DomainRule>().unwrap(), DomainRule::Any);
2722 assert_eq!(
2723 "www.example.com".parse::<DomainRule>().unwrap(),
2724 DomainRule::Exact("www.example.com".to_string())
2725 );
2726 assert_eq!(
2727 "*.example.com".parse::<DomainRule>().unwrap(),
2728 DomainRule::Wildcard("*.example.com".to_string())
2729 );
2730 assert_eq!("test.*.example.com".parse::<DomainRule>(), Err(()));
2731 assert_eq!(
2732 "/cdn[0-9]+/.example.com".parse::<DomainRule>().unwrap(),
2733 DomainRule::Regex(Regex::new("\\Acdn[0-9]+\\.example\\.com\\z").unwrap())
2734 );
2735 }
2736
2737 #[test]
2738 fn match_domain_rule() {
2739 assert!(DomainRule::Any.matches("www.example.com".as_bytes()));
2740 assert!(
2741 DomainRule::Exact("www.example.com".to_string()).matches("www.example.com".as_bytes())
2742 );
2743 assert!(
2744 DomainRule::Wildcard("*.example.com".to_string()).matches("www.example.com".as_bytes())
2745 );
2746 assert!(
2747 !DomainRule::Wildcard("*.example.com".to_string())
2748 .matches("test.www.example.com".as_bytes())
2749 );
2750 assert!(
2751 "/cdn[0-9]+/.example.com"
2752 .parse::<DomainRule>()
2753 .unwrap()
2754 .matches("cdn1.example.com".as_bytes())
2755 );
2756 assert!(
2757 !"/cdn[0-9]+/.example.com"
2758 .parse::<DomainRule>()
2759 .unwrap()
2760 .matches("www.example.com".as_bytes())
2761 );
2762 assert!(
2763 !"/cdn[0-9]+/.example.com"
2764 .parse::<DomainRule>()
2765 .unwrap()
2766 .matches("cdn10.exampleAcom".as_bytes())
2767 );
2768 }
2769
2770 #[test]
2771 fn match_domain_rule_wildcard_short_hostname_does_not_panic() {
2772 let rule = DomainRule::Wildcard("*.foo.example.com".to_string());
2773
2774 assert!(!rule.matches(b""));
2776
2777 assert!(!rule.matches(b"a.b"));
2779 assert!(!rule.matches(b"x"));
2780
2781 assert!(!rule.matches(b".foo.example.com"));
2784
2785 assert!(!rule.matches(b"y.x.foo.example.com"));
2787
2788 assert!(rule.matches(b"x.foo.example.com"));
2791 }
2792
2793 #[test]
2794 fn router_lookup_wildcard_pre_rule_short_hostname_does_not_panic() {
2795 let mut router = Router::new();
2796
2797 assert!(router.add_pre_rule(
2800 &"*.foo.example.com".parse::<DomainRule>().unwrap(),
2801 &PathRule::Prefix("/".to_string()),
2802 &MethodRule::new(Some("GET".to_string())),
2803 &Route::ClusterId("wildcard".to_string()),
2804 ));
2805
2806 let method = Method::new(&b"GET"[..]);
2807
2808 assert!(router.lookup("", "/", &method).is_err());
2810 assert!(router.lookup("x", "/", &method).is_err());
2811 assert!(router.lookup("a.b", "/", &method).is_err());
2812
2813 assert!(router.lookup(".foo.example.com", "/", &method).is_err());
2815
2816 assert_eq!(
2818 router.lookup("x.foo.example.com", "/", &method),
2819 Ok(RouteResult::forward("wildcard".to_string()))
2820 );
2821 }
2822
2823 #[test]
2824 fn match_path_rule() {
2825 assert!(PathRule::Prefix("".to_string()).matches("/".as_bytes()) != PathRuleResult::None);
2826 assert!(
2827 PathRule::Prefix("".to_string()).matches("/hello".as_bytes()) != PathRuleResult::None
2828 );
2829 assert!(
2830 PathRule::Prefix("/hello".to_string()).matches("/hello".as_bytes())
2831 != PathRuleResult::None
2832 );
2833 assert!(
2834 PathRule::Prefix("/hello".to_string()).matches("/hello/world".as_bytes())
2835 != PathRuleResult::None
2836 );
2837 assert!(
2838 PathRule::Prefix("/hello".to_string()).matches("/".as_bytes()) == PathRuleResult::None
2839 );
2840 }
2841
2842 #[test]
2850 fn multiple_children_on_a_wildcard() {
2851 let mut router = Router::new();
2852
2853 assert!(router.add_tree_rule(
2854 b"*.sozu.io",
2855 &PathRule::Prefix("".to_string()),
2856 &MethodRule::new(Some("GET".to_string())),
2857 &Route::ClusterId("base".to_string())
2858 ));
2859 println!("{:#?}", router.tree);
2860 assert_eq!(
2861 router.lookup("www.sozu.io", "/api", &Method::Get),
2862 Ok(RouteResult::forward("base".to_string()))
2863 );
2864 assert!(router.add_tree_rule(
2865 b"*.sozu.io",
2866 &PathRule::Prefix("/api".to_string()),
2867 &MethodRule::new(Some("GET".to_string())),
2868 &Route::ClusterId("api".to_string())
2869 ));
2870 println!("{:#?}", router.tree);
2871 assert_eq!(
2872 router.lookup("www.sozu.io", "/ap", &Method::Get),
2873 Ok(RouteResult::forward("base".to_string()))
2874 );
2875 assert_eq!(
2876 router.lookup("www.sozu.io", "/api", &Method::Get),
2877 Ok(RouteResult::forward("api".to_string()))
2878 );
2879 }
2880
2881 #[test]
2889 fn multiple_children_including_one_with_wildcard() {
2890 let mut router = Router::new();
2891
2892 assert!(router.add_tree_rule(
2893 b"*.sozu.io",
2894 &PathRule::Prefix("".to_string()),
2895 &MethodRule::new(Some("GET".to_string())),
2896 &Route::ClusterId("base".to_string())
2897 ));
2898 println!("{:#?}", router.tree);
2899 assert_eq!(
2900 router.lookup("www.sozu.io", "/api", &Method::Get),
2901 Ok(RouteResult::forward("base".to_string()))
2902 );
2903 assert!(router.add_tree_rule(
2904 b"api.sozu.io",
2905 &PathRule::Prefix("".to_string()),
2906 &MethodRule::new(Some("GET".to_string())),
2907 &Route::ClusterId("api".to_string())
2908 ));
2909 println!("{:#?}", router.tree);
2910 assert_eq!(
2911 router.lookup("www.sozu.io", "/api", &Method::Get),
2912 Ok(RouteResult::forward("base".to_string()))
2913 );
2914 assert_eq!(
2915 router.lookup("api.sozu.io", "/api", &Method::Get),
2916 Ok(RouteResult::forward("api".to_string()))
2917 );
2918 }
2919
2920 #[test]
2921 fn router_insert_remove_through_regex() {
2922 let mut router = Router::new();
2923
2924 assert!(router.add_tree_rule(
2925 b"www./.*/.io",
2926 &PathRule::Prefix("".to_string()),
2927 &MethodRule::new(Some("GET".to_string())),
2928 &Route::ClusterId("base".to_string())
2929 ));
2930 println!("{:#?}", router.tree);
2931 assert!(router.add_tree_rule(
2932 b"www.doc./.*/.io",
2933 &PathRule::Prefix("".to_string()),
2934 &MethodRule::new(Some("GET".to_string())),
2935 &Route::ClusterId("doc".to_string())
2936 ));
2937 println!("{:#?}", router.tree);
2938 assert_eq!(
2939 router.lookup("www.sozu.io", "/", &Method::Get),
2940 Ok(RouteResult::forward("base".to_string()))
2941 );
2942 assert_eq!(
2943 router.lookup("www.doc.sozu.io", "/", &Method::Get),
2944 Ok(RouteResult::forward("doc".to_string()))
2945 );
2946 assert!(router.remove_tree_rule(
2947 b"www./.*/.io",
2948 &PathRule::Prefix("".to_string()),
2949 &MethodRule::new(Some("GET".to_string()))
2950 ));
2951 println!("{:#?}", router.tree);
2952 assert!(router.lookup("www.sozu.io", "/", &Method::Get).is_err());
2953 assert_eq!(
2954 router.lookup("www.doc.sozu.io", "/", &Method::Get),
2955 Ok(RouteResult::forward("doc".to_string()))
2956 );
2957 }
2958
2959 #[test]
2960 fn match_router() {
2961 let mut router = Router::new();
2962
2963 assert!(router.add_pre_rule(
2964 &"*".parse::<DomainRule>().unwrap(),
2965 &PathRule::Prefix("/.well-known/acme-challenge".to_string()),
2966 &MethodRule::new(Some("GET".to_string())),
2967 &Route::ClusterId("acme".to_string())
2968 ));
2969 assert!(router.add_tree_rule(
2970 "www.example.com".as_bytes(),
2971 &PathRule::Prefix("/".to_string()),
2972 &MethodRule::new(Some("GET".to_string())),
2973 &Route::ClusterId("example".to_string())
2974 ));
2975 assert!(router.add_tree_rule(
2976 "*.test.example.com".as_bytes(),
2977 &PathRule::Regex(Regex::new("/hello[A-Z]+/").unwrap()),
2978 &MethodRule::new(Some("GET".to_string())),
2979 &Route::ClusterId("examplewildcard".to_string())
2980 ));
2981 assert!(router.add_tree_rule(
2982 "/test[0-9]/.example.com".as_bytes(),
2983 &PathRule::Prefix("/".to_string()),
2984 &MethodRule::new(Some("GET".to_string())),
2985 &Route::ClusterId("exampleregex".to_string())
2986 ));
2987
2988 assert_eq!(
2989 router.lookup("www.example.com", "/helloA", &Method::new(&b"GET"[..])),
2990 Ok(RouteResult::forward("example".to_string()))
2991 );
2992 assert_eq!(
2993 router.lookup(
2994 "www.example.com",
2995 "/.well-known/acme-challenge",
2996 &Method::new(&b"GET"[..])
2997 ),
2998 Ok(RouteResult::forward("acme".to_string()))
2999 );
3000 assert!(
3001 router
3002 .lookup("www.test.example.com", "/", &Method::new(&b"GET"[..]))
3003 .is_err()
3004 );
3005 assert_eq!(
3006 router.lookup(
3007 "www.test.example.com",
3008 "/helloAB/",
3009 &Method::new(&b"GET"[..])
3010 ),
3011 Ok(RouteResult::forward("examplewildcard".to_string()))
3012 );
3013 assert_eq!(
3014 router.lookup("test1.example.com", "/helloAB/", &Method::new(&b"GET"[..])),
3015 Ok(RouteResult::forward("exampleregex".to_string()))
3016 );
3017 }
3018
3019 #[test]
3020 fn has_hostname_checks_tree_pre_and_post() {
3021 let mut router = Router::new();
3022
3023 assert!(!router.has_hostname("www.example.com"));
3025
3026 assert!(router.add_tree_rule(
3028 b"www.example.com",
3029 &PathRule::Prefix("/".to_string()),
3030 &MethodRule::new(Some("GET".to_string())),
3031 &Route::ClusterId("cluster1".to_string())
3032 ));
3033 assert!(router.has_hostname("www.example.com"));
3034 assert!(!router.has_hostname("api.example.com"));
3035
3036 assert!(router.remove_tree_rule(
3038 b"www.example.com",
3039 &PathRule::Prefix("/".to_string()),
3040 &MethodRule::new(Some("GET".to_string()))
3041 ));
3042 assert!(!router.has_hostname("www.example.com"));
3043
3044 assert!(router.add_pre_rule(
3046 &DomainRule::Exact("api.example.com".to_string()),
3047 &PathRule::Prefix("/".to_string()),
3048 &MethodRule::new(None),
3049 &Route::ClusterId("cluster2".to_string())
3050 ));
3051 assert!(router.has_hostname("api.example.com"));
3052 assert!(!router.has_hostname("www.example.com"));
3053
3054 assert!(router.add_post_rule(
3056 &DomainRule::Exact("cdn.example.com".to_string()),
3057 &PathRule::Prefix("/".to_string()),
3058 &MethodRule::new(None),
3059 &Route::ClusterId("cluster3".to_string())
3060 ));
3061 assert!(router.has_hostname("cdn.example.com"));
3062
3063 assert!(router.remove_pre_rule(
3065 &DomainRule::Exact("api.example.com".to_string()),
3066 &PathRule::Prefix("/".to_string()),
3067 &MethodRule::new(None),
3068 ));
3069 assert!(!router.has_hostname("api.example.com"));
3070 assert!(router.has_hostname("cdn.example.com"));
3071 }
3072
3073 #[test]
3074 fn has_hostname_false_after_last_route_removed() {
3075 let mut router = Router::new();
3076
3077 assert!(router.add_tree_rule(
3079 b"www.example.com",
3080 &PathRule::Prefix("/".to_string()),
3081 &MethodRule::new(Some("GET".to_string())),
3082 &Route::ClusterId("cluster1".to_string())
3083 ));
3084 assert!(router.add_tree_rule(
3085 b"www.example.com",
3086 &PathRule::Prefix("/api".to_string()),
3087 &MethodRule::new(Some("GET".to_string())),
3088 &Route::ClusterId("cluster2".to_string())
3089 ));
3090 assert!(router.has_hostname("www.example.com"));
3091
3092 assert!(router.remove_tree_rule(
3094 b"www.example.com",
3095 &PathRule::Prefix("/".to_string()),
3096 &MethodRule::new(Some("GET".to_string()))
3097 ));
3098 assert!(router.has_hostname("www.example.com"));
3099
3100 assert!(router.remove_tree_rule(
3102 b"www.example.com",
3103 &PathRule::Prefix("/api".to_string()),
3104 &MethodRule::new(Some("GET".to_string()))
3105 ));
3106 assert!(!router.has_hostname("www.example.com"));
3107 }
3108}