postrust_proxy/saas/
wellknown_host.rs1pub fn host_matches(host_header: Option<&str>, domain: &str) -> bool {
22 let Some(host) = host_header else {
23 return false;
24 };
25 let Some(name) = strip_port(host.trim()) else {
26 return false;
27 };
28 if name.is_empty() || domain.is_empty() {
29 return false;
30 }
31 normalize(name) == normalize(domain)
32}
33
34fn strip_port(host: &str) -> Option<&str> {
39 if let Some(rest) = host.strip_prefix('[') {
40 let end = rest.find(']')?;
43 match rest[end + 1..].strip_prefix(':') {
44 Some(port) if !is_port(port) => return None,
46 None if end + 1 != rest.len() => return None,
47 _ => {}
48 }
49 return Some(&host[..end + 2]);
52 }
53
54 match host.split_once(':') {
55 Some((_, port)) if !is_port(port) => None,
57 Some((name, _)) => Some(name),
58 None => Some(host),
59 }
60}
61
62fn is_port(port: &str) -> bool {
64 !port.is_empty() && port.bytes().all(|b| b.is_ascii_digit())
65}
66
67fn normalize(name: &str) -> String {
69 name.strip_suffix('.').unwrap_or(name).to_ascii_lowercase()
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn plain_match() {
78 assert!(host_matches(Some("example.com"), "example.com"));
79 }
80
81 #[test]
82 fn mismatch_is_refused() {
83 assert!(!host_matches(Some("evil.test"), "example.com"));
84 }
85
86 #[test]
87 fn missing_header_is_refused() {
88 assert!(!host_matches(None, "example.com"));
89 }
90
91 #[test]
92 fn port_is_not_part_of_the_name() {
93 assert!(host_matches(Some("example.com:8443"), "example.com"));
94 assert!(host_matches(Some("example.com:80"), "example.com"));
95 }
96
97 #[test]
98 fn comparison_is_case_insensitive() {
99 assert!(host_matches(Some("ExAmPlE.CoM"), "example.com"));
100 assert!(host_matches(Some("example.com"), "EXAMPLE.COM"));
101 }
102
103 #[test]
104 fn trailing_dot_names_the_same_host() {
105 assert!(host_matches(Some("example.com."), "example.com"));
106 assert!(host_matches(Some("example.com.:443"), "example.com"));
107 }
108
109 #[test]
110 fn ipv6_literal_keeps_its_brackets() {
111 assert!(host_matches(Some("[::1]:8443"), "[::1]"));
112 assert!(host_matches(Some("[::1]"), "[::1]"));
113 }
114
115 #[test]
116 fn unterminated_ipv6_literal_is_refused() {
117 assert!(!host_matches(Some("[::1:8443"), "[::1]"));
118 }
119
120 #[test]
121 fn non_numeric_port_is_refused() {
122 assert!(!host_matches(Some("example.com:https"), "example.com"));
123 }
124
125 #[test]
126 fn a_second_colon_is_refused() {
127 assert!(!host_matches(Some("example.com:80:80"), "example.com"));
128 }
129
130 #[test]
131 fn empty_inputs_are_refused() {
132 assert!(!host_matches(Some(""), "example.com"));
133 assert!(!host_matches(Some("example.com"), ""));
134 assert!(!host_matches(Some(":8443"), "example.com"));
135 }
136
137 #[test]
138 fn surrounding_whitespace_is_tolerated() {
139 assert!(host_matches(Some(" example.com "), "example.com"));
140 }
141
142 #[test]
143 fn a_prefix_of_the_domain_is_not_a_match() {
144 assert!(!host_matches(Some("example.com.evil.test"), "example.com"));
146 assert!(!host_matches(Some("notexample.com"), "example.com"));
147 }
148}