use core::net::IpAddr;
use bun_core::strings;
fn strip_trailing_dot(host: &[u8]) -> &[u8] {
host.strip_suffix(b".").unwrap_or(host)
}
fn parse_ip(text: &[u8]) -> Option<IpAddr> {
bun_core::fmt::parse_ascii::<IpAddr>(text).map(|ip| ip.to_canonical())
}
fn split_port(entry: &[u8]) -> (&[u8], Option<&[u8]>) {
if entry.first() == Some(&b'[') {
if let Some(close) = strings::index_of_char_usize(entry, b']') {
let rest = &entry[close + 1..];
return (&entry[1..close], rest.strip_prefix(b":"));
}
return (entry, None);
}
if strings::count_char(entry, b':') == 1 {
let colon = strings::index_of_char_usize(entry, b':').unwrap();
return (&entry[..colon], Some(&entry[colon + 1..]));
}
(entry, None)
}
fn parse_digits<T: TryFrom<i128> + TryFrom<u128>>(text: &[u8]) -> Option<T> {
if text.is_empty() || !text.iter().all(u8::is_ascii_digit) {
return None;
}
bun_core::fmt::parse_unsigned::<T>(text, 10).ok()
}
fn cidr_contains(entry: &[u8], slash: usize, host: IpAddr) -> bool {
let Some(written) = bun_core::fmt::parse_ascii::<IpAddr>(bun_url::strip_ipv6_brackets(
&entry[..slash],
)) else {
return false;
};
let Some(mut bits) = parse_digits::<u8>(&entry[slash + 1..]) else {
return false;
};
let network = written.to_canonical();
if written.is_ipv6() && network.is_ipv4() {
let Some(v4_bits) = bits.checked_sub(96) else {
return false;
};
bits = v4_bits;
}
fn prefix_eq(a: &[u8], b: &[u8], bits: u8) -> bool {
let bits = bits as usize;
if bits > a.len() * 8 {
return false;
}
let (whole, rem) = (bits / 8, bits % 8);
if a[..whole] != b[..whole] {
return false;
}
rem == 0 || (a[whole] ^ b[whole]) >> (8 - rem) == 0
}
match (network, host) {
(IpAddr::V4(n), IpAddr::V4(h)) => prefix_eq(&n.octets(), &h.octets(), bits),
(IpAddr::V6(n), IpAddr::V6(h)) => prefix_eq(&n.octets(), &h.octets(), bits),
_ => false,
}
}
fn entries(list: &[u8]) -> impl Iterator<Item = &[u8]> {
struct Entries<'a> {
rest: &'a [u8],
}
impl<'a> Iterator for Entries<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<Self::Item> {
let rest = self.rest;
let start = rest
.iter()
.position(|b| !matches!(b, b',' | b' ' | b'\t' | b'\r' | b'\n'))?;
let rest = &rest[start..];
let end = rest
.iter()
.position(|b| matches!(b, b',' | b' ' | b'\t' | b'\r' | b'\n'))
.unwrap_or(rest.len());
self.rest = &rest[end..];
Some(&rest[..end])
}
}
Entries { rest: list }
}
pub fn matches(list: &[u8], hostname: &[u8], port: u16) -> bool {
let hostname = strip_trailing_dot(bun_url::strip_ipv6_brackets(hostname));
if hostname.is_empty() {
return false;
}
let mut iter = entries(list).peekable();
if iter.peek().is_none() {
return false;
}
let host_ip = parse_ip(hostname);
for entry in iter {
if entry == b"*" {
return true;
}
if let Some(slash) = strings::index_of_char_usize(entry, b'/') {
if host_ip.is_some_and(|ip| cidr_contains(entry, slash, ip)) {
return true;
}
continue;
}
let (entry_host, entry_port) = split_port(entry);
if let Some(entry_port) = entry_port {
match parse_digits::<u16>(entry_port) {
Some(p) if p == port => {}
_ => continue,
}
}
let entry_host = entry_host
.strip_prefix(b"*.")
.or_else(|| entry_host.strip_prefix(b"."))
.unwrap_or(entry_host);
let entry_host = strip_trailing_dot(entry_host);
if entry_host.is_empty() {
continue;
}
if let Some(ip) = host_ip {
if parse_ip(entry_host) == Some(ip) {
return true;
}
continue;
}
if hostname.len() == entry_host.len() {
if strings::eql_case_insensitive_ascii(hostname, entry_host, true) {
return true;
}
} else if hostname.len() > entry_host.len()
&& hostname[hostname.len() - entry_host.len() - 1] == b'.'
&& strings::eql_case_insensitive_ascii(
&hostname[hostname.len() - entry_host.len()..],
entry_host,
true,
)
{
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::matches;
#[test]
fn star_matches_any_host() {
assert!(matches(b"*", b"example.com", 80));
assert!(matches(b"*", b"10.0.0.1", 80));
assert!(matches(b"*", b"[::1]", 80));
assert!(!matches(b"*:8080", b"example.com", 8080));
assert!(!matches(b"*:8080", b"example.com", 443));
}
#[test]
fn bare_domain_matches_self_and_subdomains_only() {
assert!(matches(b"example.com", b"example.com", 443));
assert!(matches(b"example.com", b"sub.example.com", 443));
assert!(matches(b"example.com", b"a.b.example.com", 443));
assert!(!matches(b"example.com", b"notexample.com", 443));
assert!(!matches(b"example.com", b"example.com.evil.io", 443));
}
#[test]
fn leading_dot_and_wildcard_forms() {
for list in [b".example.com".as_slice(), b"*.example.com".as_slice()] {
assert!(matches(list, b"example.com", 80), "{:?}", list);
assert!(matches(list, b"sub.example.com", 80), "{:?}", list);
assert!(!matches(list, b"notexample.com", 80), "{:?}", list);
assert!(!matches(list, b"example.org", 80), "{:?}", list);
}
}
#[test]
fn case_insensitive_and_trailing_dot() {
assert!(matches(b"EXAMPLE.com", b"example.com", 80));
assert!(matches(b"example.com", b"SUB.Example.COM", 80));
assert!(matches(b"example.com", b"example.com.", 80));
assert!(!matches(b".", b"example.com", 80));
}
#[test]
fn ipv4_cidr() {
assert!(matches(b"10.0.0.0/8", b"10.1.2.3", 80));
assert!(!matches(b"10.0.0.0/8", b"11.0.0.1", 80));
assert!(matches(b"192.168.1.0/24", b"192.168.1.55", 80));
assert!(!matches(b"192.168.1.0/24", b"192.168.2.1", 80));
assert!(matches(b"192.168.1.1/32", b"192.168.1.1", 80));
assert!(!matches(b"192.168.1.1/32", b"192.168.1.2", 80));
}
#[test]
fn ipv6_cidr_bare_and_bracketed_host() {
assert!(matches(b"fd00::/8", b"fd00::1", 80));
assert!(matches(b"fd00::/8", b"[fd12::1]", 80));
assert!(!matches(b"fd00::/8", b"fe00::1", 80));
assert!(!matches(b"fd00::/8", b"fd00.example.com", 80));
}
#[test]
fn v4_mapped_v6_cidr_folds_to_v4_prefix() {
assert!(matches(b"::ffff:10.0.0.0/104", b"10.0.0.5", 80));
assert!(matches(b"::ffff:10.0.0.0/104", b"10.1.0.5", 80));
assert!(!matches(b"::ffff:10.0.0.0/104", b"11.0.0.5", 80));
}
#[test]
fn bracketed_and_bare_ipv6_entries_and_host() {
assert!(matches(b"[::1]", b"[::1]", 80));
assert!(matches(b"[::1]", b"::1", 80));
assert!(matches(b"::1", b"[::1]", 80));
assert!(!matches(b"::1", b"::2", 80));
assert!(matches(b"[example.com]", b"example.com", 80));
}
#[test]
fn host_port_compares_effective_port() {
assert!(matches(b"example.com:8080", b"example.com", 8080));
assert!(!matches(b"example.com:8080", b"example.com", 443));
assert!(matches(b"example.com", b"example.com", 9999));
assert!(matches(b".example.com:8080", b"sub.example.com", 8080));
assert!(!matches(b".example.com:8080", b"sub.example.com", 80));
}
#[test]
fn bracketed_v6_entry_with_port() {
assert!(matches(b"[::1]:8080", b"[::1]", 8080));
assert!(matches(b"[::1]:8080", b"::1", 8080));
assert!(!matches(b"[::1]:8080", b"::1", 443));
}
#[test]
fn ip_literal_host_never_matches_domain_suffix() {
for entry in [
b"0.0.1".as_slice(),
b".0.0.1",
b"1",
b".1",
b"com",
b"127.0.0.1.example.com",
b"example.com",
] {
assert!(
!matches(entry, b"127.0.0.1", 80),
"IP host must not match domain entry {:?}",
entry
);
}
assert!(matches(b"127.0.0.1", b"127.0.0.1", 80));
assert!(matches(b"127.0.0.0/8", b"127.0.0.1", 80));
}
#[test]
fn resolver_shorthand_is_not_an_address() {
assert!(!matches(b"127.1", b"127.0.0.1", 80));
assert!(!matches(b"0x7f.1", b"127.0.0.1", 80));
assert!(matches(b"127.1", b"127.1", 80));
assert!(!matches(b"127.0.0.1", b"127.1", 80));
}
#[test]
fn mixed_comma_and_whitespace_separators() {
let list = b" a.com,b.com\tc.com\r13.0.0.0/24\nd.com";
assert!(matches(list, b"a.com", 80));
assert!(matches(list, b"b.com", 80));
assert!(matches(list, b"c.com", 80));
assert!(matches(list, b"d.com", 80));
assert!(matches(list, b"13.0.0.9", 80));
assert!(!matches(list, b"e.com", 80));
}
#[test]
fn port_and_prefix_digits_only() {
assert!(!matches(b"example.com:8_0", b"example.com", 80));
assert!(!matches(b"example.com:80x", b"example.com", 80));
assert!(!matches(b"example.com:", b"example.com", 80));
assert!(!matches(b"example.com:+80", b"example.com", 80));
assert!(!matches(b"10.0.0.0/8x", b"10.1.2.3", 80));
assert!(!matches(b"10.0.0.0/", b"10.1.2.3", 80));
assert!(!matches(b"10.0.0.0/999", b"10.1.2.3", 80));
assert!(matches(b"10.0.0.0/08", b"10.1.2.3", 80));
}
#[test]
fn empty_list_or_empty_host_never_matches() {
assert!(!matches(b"", b"example.com", 80));
assert!(!matches(b" ", b"example.com", 80));
assert!(!matches(b",,,", b"example.com", 80));
assert!(!matches(b"example.com", b"", 80));
assert!(!matches(b"example.com", b".", 80));
}
}