fix: normalize_host handles IPv6 bracket notation

Extract strip_port_from_host into shared utils module and update normalize_host to properly strip brackets from IPv6 addresses like [::1]:443 -> ::1 instead of incorrectly using split(':').next().
This commit is contained in:
2026-06-12 04:40:43 +00:00
parent d7f811ffb5
commit 42c721e954
4 changed files with 78 additions and 40 deletions

View File

@@ -50,8 +50,14 @@ pub fn build_routing_table(sites: &[SiteConfig]) -> HashMap<String, SiteConfig>
}
pub fn normalize_host(host: &str) -> String {
let lower = host.to_lowercase();
lower.split(':').next().unwrap_or(&lower).to_string()
let stripped = crate::utils::strip_port_from_host(host);
let lower = stripped.to_lowercase();
lower
.strip_prefix('[')
.unwrap_or(&lower)
.strip_suffix(']')
.unwrap_or(&lower)
.to_string()
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
@@ -304,6 +310,26 @@ mod tests {
assert_eq!(normalize_host(""), "");
}
#[test]
fn normalize_host_ipv6_with_port() {
assert_eq!(normalize_host("[::1]:443"), "::1");
}
#[test]
fn normalize_host_ipv6_long_with_port() {
assert_eq!(normalize_host("[2001:db8::1]:8080"), "2001:db8::1");
}
#[test]
fn normalize_host_ipv6_bare() {
assert_eq!(normalize_host("[::1]"), "::1");
}
#[test]
fn normalize_host_ipv6_uppercase() {
assert_eq!(normalize_host("[2001:DB8::1]:443"), "2001:db8::1");
}
#[test]
fn routing_table_lookup_finds_site() {
let config = test_fixtures::test_dynamic_config();

View File

@@ -8,3 +8,4 @@ pub mod rate_limit;
pub mod server;
pub mod shutdown;
pub mod tls;
pub mod utils;

View File

@@ -13,19 +13,7 @@ use crate::config::static_config::ListenerConfig;
const ACME_CHALLENGE_PREFIX: &str = "/.well-known/acme-challenge/";
fn strip_port_from_host(host: &str) -> &str {
if host.starts_with('[') {
if let Some(bracket_end) = host.find(']') {
&host[..bracket_end + 1]
} else {
host
}
} else if let Some(colon_pos) = host.rfind(':') {
&host[..colon_pos]
} else {
host
}
}
use crate::utils::strip_port_from_host;
pub fn build_redirect_url(host: &str, https_port: u16, path: &str, query: &str) -> String {
let hostname = strip_port_from_host(host);
@@ -218,29 +206,4 @@ mod tests {
let url = build_redirect_url("203.0.113.10", 443, "/", "");
assert_eq!(url, "https://203.0.113.10/");
}
#[test]
fn test_strip_port_from_host_plain() {
assert_eq!(strip_port_from_host("example.com"), "example.com");
}
#[test]
fn test_strip_port_from_host_with_port() {
assert_eq!(strip_port_from_host("example.com:8080"), "example.com");
}
#[test]
fn test_strip_port_from_host_ipv6_bare() {
assert_eq!(strip_port_from_host("[::1]"), "[::1]");
}
#[test]
fn test_strip_port_from_host_ipv6_with_port() {
assert_eq!(strip_port_from_host("[::1]:8080"), "[::1]");
}
#[test]
fn test_strip_port_from_host_ipv4_with_port() {
assert_eq!(strip_port_from_host("192.168.1.1:8080"), "192.168.1.1");
}
}

48
src/utils.rs Normal file
View File

@@ -0,0 +1,48 @@
pub fn strip_port_from_host(host: &str) -> &str {
if host.starts_with('[') {
if let Some(bracket_end) = host.find(']') {
&host[..bracket_end + 1]
} else {
host
}
} else if let Some(colon_pos) = host.rfind(':') {
&host[..colon_pos]
} else {
host
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_port_plain() {
assert_eq!(strip_port_from_host("example.com"), "example.com");
}
#[test]
fn strip_port_with_port() {
assert_eq!(strip_port_from_host("example.com:8080"), "example.com");
}
#[test]
fn strip_port_ipv6_bare() {
assert_eq!(strip_port_from_host("[::1]"), "[::1]");
}
#[test]
fn strip_port_ipv6_with_port() {
assert_eq!(strip_port_from_host("[::1]:8080"), "[::1]");
}
#[test]
fn strip_port_ipv6_with_long_address() {
assert_eq!(strip_port_from_host("[2001:db8::1]:8080"), "[2001:db8::1]");
}
#[test]
fn strip_port_ipv4_with_port() {
assert_eq!(strip_port_from_host("192.168.1.1:8080"), "192.168.1.1");
}
}