use crate::saas::validation::normalize_domain;
pub fn host_matches(host_header: Option<&str>, domain: &str) -> bool {
let Some(host) = host_header else {
return false;
};
let Some(name) = strip_port(host.trim()) else {
return false;
};
match (normalize_domain(name), normalize_domain(domain)) {
(Ok(name), Ok(domain)) => name == domain,
_ => false,
}
}
fn strip_port(host: &str) -> Option<&str> {
if let Some(rest) = host.strip_prefix('[') {
let end = rest.find(']')?;
match rest[end + 1..].strip_prefix(':') {
Some(port) if !is_port(port) => return None,
None if end + 1 != rest.len() => return None,
_ => {}
}
return Some(&host[..end + 2]);
}
match host.split_once(':') {
Some((_, port)) if !is_port(port) => None,
Some((name, _)) => Some(name),
None => Some(host),
}
}
fn is_port(port: &str) -> bool {
!port.is_empty() && port.bytes().all(|b| b.is_ascii_digit())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_match() {
assert!(host_matches(Some("example.com"), "example.com"));
}
#[test]
fn mismatch_is_refused() {
assert!(!host_matches(Some("evil.test"), "example.com"));
}
#[test]
fn missing_header_is_refused() {
assert!(!host_matches(None, "example.com"));
}
#[test]
fn port_is_not_part_of_the_name() {
assert!(host_matches(Some("example.com:8443"), "example.com"));
assert!(host_matches(Some("example.com:80"), "example.com"));
}
#[test]
fn comparison_is_case_insensitive() {
assert!(host_matches(Some("ExAmPlE.CoM"), "example.com"));
assert!(host_matches(Some("example.com"), "EXAMPLE.COM"));
}
#[test]
fn trailing_dot_names_the_same_host() {
assert!(host_matches(Some("example.com."), "example.com"));
assert!(host_matches(Some("example.com.:443"), "example.com"));
}
#[test]
fn ip_literals_never_match() {
assert!(!host_matches(Some("[::1]:8443"), "[::1]"));
assert!(!host_matches(Some("[::1]"), "[::1]"));
assert!(!host_matches(Some("127.0.0.1"), "127.0.0.1"));
}
#[test]
fn a_case_variant_registration_is_the_same_name() {
assert!(host_matches(Some("victim.test"), "VICTIM.test"));
assert!(host_matches(Some("VICTIM.TEST."), "victim.test"));
}
#[test]
fn unterminated_ipv6_literal_is_refused() {
assert!(!host_matches(Some("[::1:8443"), "[::1]"));
}
#[test]
fn non_numeric_port_is_refused() {
assert!(!host_matches(Some("example.com:https"), "example.com"));
}
#[test]
fn a_second_colon_is_refused() {
assert!(!host_matches(Some("example.com:80:80"), "example.com"));
}
#[test]
fn empty_inputs_are_refused() {
assert!(!host_matches(Some(""), "example.com"));
assert!(!host_matches(Some("example.com"), ""));
assert!(!host_matches(Some(":8443"), "example.com"));
}
#[test]
fn surrounding_whitespace_is_tolerated() {
assert!(host_matches(Some(" example.com "), "example.com"));
}
#[test]
fn a_prefix_of_the_domain_is_not_a_match() {
assert!(!host_matches(Some("example.com.evil.test"), "example.com"));
assert!(!host_matches(Some("notexample.com"), "example.com"));
}
}