postrust_proxy/saas/
wellknown_host.rs1use crate::saas::validation::normalize_domain;
7
8pub fn host_matches(host_header: Option<&str>, domain: &str) -> bool {
29 let Some(host) = host_header else {
30 return false;
31 };
32 let Some(name) = strip_port(host.trim()) else {
33 return false;
34 };
35 match (normalize_domain(name), normalize_domain(domain)) {
36 (Ok(name), Ok(domain)) => name == domain,
37 _ => false,
38 }
39}
40
41fn strip_port(host: &str) -> Option<&str> {
46 if let Some(rest) = host.strip_prefix('[') {
47 let end = rest.find(']')?;
50 match rest[end + 1..].strip_prefix(':') {
51 Some(port) if !is_port(port) => return None,
53 None if end + 1 != rest.len() => return None,
54 _ => {}
55 }
56 return Some(&host[..end + 2]);
59 }
60
61 match host.split_once(':') {
62 Some((_, port)) if !is_port(port) => None,
64 Some((name, _)) => Some(name),
65 None => Some(host),
66 }
67}
68
69fn is_port(port: &str) -> bool {
71 !port.is_empty() && port.bytes().all(|b| b.is_ascii_digit())
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn plain_match() {
80 assert!(host_matches(Some("example.com"), "example.com"));
81 }
82
83 #[test]
84 fn mismatch_is_refused() {
85 assert!(!host_matches(Some("evil.test"), "example.com"));
86 }
87
88 #[test]
89 fn missing_header_is_refused() {
90 assert!(!host_matches(None, "example.com"));
91 }
92
93 #[test]
94 fn port_is_not_part_of_the_name() {
95 assert!(host_matches(Some("example.com:8443"), "example.com"));
96 assert!(host_matches(Some("example.com:80"), "example.com"));
97 }
98
99 #[test]
100 fn comparison_is_case_insensitive() {
101 assert!(host_matches(Some("ExAmPlE.CoM"), "example.com"));
102 assert!(host_matches(Some("example.com"), "EXAMPLE.COM"));
103 }
104
105 #[test]
106 fn trailing_dot_names_the_same_host() {
107 assert!(host_matches(Some("example.com."), "example.com"));
108 assert!(host_matches(Some("example.com.:443"), "example.com"));
109 }
110
111 #[test]
112 fn ip_literals_never_match() {
113 assert!(!host_matches(Some("[::1]:8443"), "[::1]"));
117 assert!(!host_matches(Some("[::1]"), "[::1]"));
118 assert!(!host_matches(Some("127.0.0.1"), "127.0.0.1"));
119 }
120
121 #[test]
122 fn a_case_variant_registration_is_the_same_name() {
123 assert!(host_matches(Some("victim.test"), "VICTIM.test"));
127 assert!(host_matches(Some("VICTIM.TEST."), "victim.test"));
128 }
129
130 #[test]
131 fn unterminated_ipv6_literal_is_refused() {
132 assert!(!host_matches(Some("[::1:8443"), "[::1]"));
133 }
134
135 #[test]
136 fn non_numeric_port_is_refused() {
137 assert!(!host_matches(Some("example.com:https"), "example.com"));
138 }
139
140 #[test]
141 fn a_second_colon_is_refused() {
142 assert!(!host_matches(Some("example.com:80:80"), "example.com"));
143 }
144
145 #[test]
146 fn empty_inputs_are_refused() {
147 assert!(!host_matches(Some(""), "example.com"));
148 assert!(!host_matches(Some("example.com"), ""));
149 assert!(!host_matches(Some(":8443"), "example.com"));
150 }
151
152 #[test]
153 fn surrounding_whitespace_is_tolerated() {
154 assert!(host_matches(Some(" example.com "), "example.com"));
155 }
156
157 #[test]
158 fn a_prefix_of_the_domain_is_not_a_match() {
159 assert!(!host_matches(Some("example.com.evil.test"), "example.com"));
161 assert!(!host_matches(Some("notexample.com"), "example.com"));
162 }
163}