impl Snapshot { fn find_host_pattern(host: &str, mut lookup: impl FnMut(&str) -> Option) -> Option { if let Some(value) = lookup(host) { return Some(value); } let last_dot = host.rfind('.')?; let mut wildcard = [0u8; 256]; wildcard[..2].copy_from_slice(b"*."); for (index, _) in host.match_indices('.') { if index == last_dot { break; } let suffix = &host[index + 1..]; let len = suffix.len() + 2; if len <= wildcard.len() { wildcard[2..len].copy_from_slice(suffix.as_bytes()); let key = std::str::from_utf8(&wildcard[..len]) .expect("a wildcard plus a UTF-8 host is valid UTF-8"); if let Some(value) = lookup(key) { return Some(value); } } else if let Some(value) = lookup(&format!("*.{suffix}")) { return Some(value); } } None } pub fn host_allowed(&self, host: &str, peer: IpAddr) -> bool { if self.host_acls.is_empty() { return true; } Self::find_host_pattern(host, |pattern| { self.host_acls.get(pattern).map(|acl| acl.allows(peer)) }) .unwrap_or(true) } pub fn websocket_rfc8441_on(&self, local: SocketAddr, tls: bool) -> bool { self.frontends.values().flatten().any(|frontend| { frontend.websockets && frontend.websocket_rfc8441 && frontend.http2_enabled && frontend.bindings.iter().any(|binding| { binding.matches(local, tls) && if tls { binding.http2 } else { binding.h2c } }) }) } pub fn websocket_rfc9220_on(&self, local: SocketAddr) -> bool { self.frontends.values().flatten().any(|frontend| { frontend.websockets && frontend.websocket_rfc9220 && frontend.http3_enabled && frontend.bindings.iter().any(|binding| { binding.tls && binding.http3.is_some() && binding.matches(local, true) }) }) } pub fn select_on( &self, host: &str, path: &str, local: SocketAddr, tls: bool, ) -> Option<&CompiledRoute> { self.select_on_arc(host, path, local, tls).map(Arc::as_ref) } pub fn select_on_arc( &self, host: &str, path: &str, local: SocketAddr, tls: bool, ) -> Option<&Arc> { Self::find_host_pattern(host, |pattern| { let fallback = self.fallbacks.get(pattern).and_then(|routes| { routes .iter() .find(|route| route.bindings.iter().any(|l| l.matches(local, tls))) })?; self.routes .get(pattern) .and_then(|routes| { select_route(routes, path, |r| { r.bindings.iter().any(|l| l.matches(local, tls)) }) }) .or(Some(fallback)) }) } pub fn frontend( &self, host: &str, local: SocketAddr, tls: bool, ) -> Option<&Arc> { Self::find_host_pattern(host, |pattern| { self.frontends.get(pattern).and_then(|frontends| { frontends .iter() .find(|f| f.bindings.iter().any(|l| l.matches(local, tls))) }) }) } pub fn resolve_logging_vhost( &self, local: SocketAddr, tls: bool, routed_vhost: Option<&str>, authority: Option<&str>, host: Option<&str>, sni: Option<&str>, ) -> Option { if let Some(routed) = routed_vhost.filter(|value| !value.is_empty()) { if self.frontends.values().flatten().any(|frontend| { frontend.id == routed && frontend .bindings .iter() .any(|binding| binding.matches(local, tls)) }) { return Some(ResolvedLogVhost { vhost: routed.to_owned(), matched_by: "router", }); } } for (candidate, matched_by) in [(authority, "authority"), (host, "host"), (sni, "sni")] { let Some(candidate) = candidate else { continue; }; let Ok(normalized) = request_host(candidate) else { continue; }; if let Some(frontend) = self.frontend(&normalized, local, tls) { return Some(ResolvedLogVhost { vhost: frontend.id.clone(), matched_by, }); } } None } pub fn redirect_on( &self, host: &str, path: &str, local: SocketAddr, tls: bool, ) -> Option<( &Arc, &crate::redirect::CompiledRule, )> { if self.redirects.is_empty() { return None; } Self::find_host_pattern(host, |pattern| { self.redirects.get(pattern).and_then(|redirects| { redirects.iter().find_map(|redirect| { if !redirect .bindings .iter() .any(|binding| binding.matches(local, tls)) { return None; } redirect .rules .iter() .find(|rule| rule.matches(path, tls)) .map(|rule| (redirect, rule)) }) }) }) } pub fn not_found_host_on( &self, host: &str, local: SocketAddr, tls: bool, ) -> Option<&Arc> { if self.not_found_hosts.is_empty() { return None; } Self::find_host_pattern(host, |pattern| { self.not_found_hosts.get(pattern).and_then(|items| { items.iter().find(|item| { item.bindings .iter() .any(|binding| binding.matches(local, tls)) }) }) }) } pub fn select(&self, host: &str, path: &str) -> Option<&CompiledRoute> { Self::find_host_pattern(host, |pattern| { let fallback = self .fallbacks .get(pattern) .and_then(|routes| routes.first())?; self.routes .get(pattern) .and_then(|routes| select_route(routes, path, |_| true)) .or(Some(fallback)) .map(Arc::as_ref) }) } } fn select_route<'a, F>( routes: &'a [Arc], path: &str, allowed: F, ) -> Option<&'a Arc> where F: Fn(&CompiledRoute) -> bool, { if let Some(exact) = routes.iter().find(|route| { !route.internal && allowed(route) && route.match_type == crate::vhost::PathMatch::Exact && route.path_prefix == path }) { return Some(exact); } let prefix = routes .iter() .filter(|route| { !route.internal && allowed(route) && matches!( route.match_type, crate::vhost::PathMatch::Prefix | crate::vhost::PathMatch::PrefixPriority ) && prefix_matches(&route.path_prefix, path) }) .max_by_key(|route| route.path_prefix.len()); if prefix.is_some_and(|route| route.match_type == crate::vhost::PathMatch::PrefixPriority) { return prefix; } if let Some(regex) = routes.iter().find(|route| { !route.internal && allowed(route) && matches!( route.match_type, crate::vhost::PathMatch::Regex | crate::vhost::PathMatch::RegexInsensitive ) && route .path_regex .as_ref() .is_some_and(|regex| regex.is_match(path)) }) { return Some(regex); } prefix } impl CompiledRoute { pub fn redirect_location( &self, path: &str, uri: &http::Uri, ) -> Result> { let Some(spec) = &self.redirect else { return Ok(None); }; let target = if let Some(regex) = &spec.rewrite_regex { ensure!( regex.is_match(path), "location redirect rewrite regex did not match" ); regex.replace(path, spec.target.as_str()).into_owned() } else { spec.target.clone() }; let mut target = url::Url::parse(&target).context("invalid compiled location redirect target")?; if spec.preserve_query { if let Some(query) = uri.query() { let merged = match target.query() { Some(existing) if !existing.is_empty() => format!("{existing}&{query}"), _ => query.to_owned(), }; target.set_query(Some(&merged)); } } Ok(Some( http::HeaderValue::from_str(target.as_str()) .context("invalid redirect Location header")?, )) } pub fn rewritten_path(&self, path: &str) -> Result> { use crate::vhost::RewriteMode; let rewritten = match self.rewrite.mode { RewriteMode::Preserve => return Ok(None), RewriteMode::StripPrefix => { let tail = path.strip_prefix(&self.path_prefix).unwrap_or(""); if tail.is_empty() { "/".to_owned() } else { tail.to_owned() } } RewriteMode::ReplacePrefix => { let tail = path.strip_prefix(&self.path_prefix).unwrap_or(""); let base = self.rewrite.value.trim_end_matches('/'); if tail.is_empty() { if base.is_empty() { "/".to_owned() } else { base.to_owned() } } else if base.is_empty() || base == "/" { tail.to_owned() } else { format!("{base}{tail}") } } RewriteMode::Regex => self .path_regex .as_ref() .expect("regex rewrite was validated") .replace(path, self.rewrite.value.as_str()) .into_owned(), }; crate::acl::validate_decoded_routing_path(&rewritten) .context("invalid rewritten location path")?; Ok(Some(rewritten)) } } pub fn prefix_matches(prefix: &str, path: &str) -> bool { prefix == "/" || path == prefix || path .strip_prefix(prefix) .is_some_and(|tail| tail.starts_with('/')) } pub(crate) fn valid_id(id: &str) -> bool { !id.is_empty() && id.len() <= 64 && id .bytes() .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') } pub fn normalize_host(value: &str) -> Result { let host = value .strip_suffix('.') .unwrap_or(value) .to_ascii_lowercase(); ensure!( !host.is_empty() && host.len() <= 253, "hostname length must be 1..253" ); ensure!( host.split('.').all(|label| { !label.is_empty() && label.len() <= 63 && !label.starts_with('-') && !label.ends_with('-') && label .bytes() .all(|b| b.is_ascii_alphanumeric() || b == b'-') }), "invalid hostname: use an ASCII hostname without a port or wildcard" ); Ok(host) } pub fn normalize_host_pattern(value: &str) -> Result { let value = value.trim(); if let Some(base) = value.strip_prefix("*.") { ensure!( !base.contains('*'), "wildcard hostname allows only one leading *." ); let base = normalize_host(base)?; ensure!( base.contains('.') && base.parse::().is_err(), "wildcard hostname requires a DNS suffix such as *.example.com" ); return Ok(format!("*.{base}")); } ensure!( !value.contains('*'), "wildcard hostname allows only one leading *." ); normalize_host(value) } pub fn request_host(value: &str) -> Result { ensure!(!value.contains('@'), "invalid Host"); let authority: Authority = value.parse().context("invalid Host authority")?; let raw_host = authority.host(); let suffix = &authority.as_str()[raw_host.len()..]; if !suffix.is_empty() { let port = suffix.strip_prefix(':').context("invalid Host port")?; if !port.is_empty() { let number: u16 = port.parse().context("invalid Host port")?; ensure!(number > 0, "invalid Host port"); } } let host = raw_host.trim_start_matches('[').trim_end_matches(']'); if let Ok(ip) = host.parse::() { return Ok(ip.to_string()); } normalize_host(host) } fn ipv4_policy(ip: Ipv4Addr, allow_private: bool) -> Result<()> { let [a, b, _, _] = ip.octets(); ensure!( a != 0 && a < 224 && !ip.is_broadcast(), "unspecified/reserved/multicast target blocked" ); ensure!( !(a == 169 && b == 254), "link-local and metadata targets are blocked" ); ensure!( !(a == 100 && (64..=127).contains(&b)), "shared-address/metadata range is blocked" ); ensure!( !(a == 198 && (b == 18 || b == 19)), "benchmark range is blocked" ); ensure!( !ip.is_documentation(), "documentation address is not a backend" ); ensure!( allow_private || (!ip.is_private() && !ip.is_loopback()), "private/loopback backend requires allow_private=true" ); Ok(()) } pub(crate) fn ip_policy(ip: IpAddr, allow_private: bool) -> Result<()> { match ip { IpAddr::V4(ip) => ipv4_policy(ip, allow_private), IpAddr::V6(ip) => { if let Some(v4) = ip.to_ipv4_mapped() { return ipv4_policy(v4, allow_private); } let s = ip.segments(); ensure!( !ip.is_unspecified() && !ip.is_multicast(), "unspecified/multicast target blocked" ); ensure!( s[0] & 0xffc0 != 0xfe80, "IPv6 link-local targets are blocked" ); ensure!( !(s[0] == 0x2001 && s[1] == 0x0db8), "documentation address is not a backend" ); let ula = s[0] & 0xfe00 == 0xfc00; let global = s[0] & 0xe000 == 0x2000 && s[0] != 0x2002 && !(s[0] == 0x2001 && s[1] == 0); ensure!( global || ula || ip.is_loopback(), "special IPv6 target blocked" ); ensure!( allow_private || (!ula && !ip.is_loopback()), "private/loopback backend requires allow_private=true" ); Ok(()) } } } pub(crate) fn compile_backend( backend: &Backend, policy: &ValidationPolicy, bindings: &[SocketAddr], keepalive: &crate::balance::Keepalive, ) -> Result { compile_backend_with_timeout( backend, policy, bindings, keepalive, std::time::Duration::from_secs(10), ) }