use std::{
cell::LazyCell,
fmt,
hash::{Hash, Hasher},
str::FromStr,
};
use either::Either;
use http::{
Uri,
uri::{Authority, PathAndQuery},
};
use peg::{error::ParseError, str::LineCol};
use super::BindHost;
use crate::dquic::{
net::Family,
qinterface::bind_uri::{BindUri, Scheme},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BindPattern {
pub scheme: Scheme,
pub host: BindHost,
pub port: Option<u16>,
pub path_and_query: Option<PathAndQuery>,
}
impl Hash for BindPattern {
fn hash<H: Hasher>(&self, state: &mut H) {
self.scheme.hash(state);
self.host.hash(state);
self.port.hash(state);
self.path_and_query
.as_ref()
.map(|pq| pq.as_str())
.hash(state);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn match_helpers_reject_uri_of_other_shape() {
let iface_pattern: BindPattern = "iface://v4.en*:8080".parse().unwrap();
let inet_uri: BindUri = "inet://127.0.0.1:8080".parse().unwrap();
assert!(!iface_pattern.matches_iface_bind_uri(&inet_uri));
let inet_pattern: BindPattern = "inet://127.0.0.1:8080".parse().unwrap();
let iface_uri: BindUri = "iface://v4.enp17s0:8080".parse().unwrap();
assert!(!inet_pattern.matches_inet_bind_uri(&iface_uri));
}
#[test]
fn ip_hosts_do_not_match_interface_links() {
let pattern: BindPattern = "127.0.0.1:8080".parse().unwrap();
assert_eq!(pattern.match_interface_links("lo").count(), 0);
}
#[test]
fn interface_bind_uris_expand_only_iface_patterns() {
let iface_pattern: BindPattern = "iface://v4.lo:8080".parse().unwrap();
let inet_pattern: BindPattern = "inet://127.0.0.1:8080".parse().unwrap();
let iface_uris: Vec<_> = iface_pattern
.interface_bind_uris("lo")
.map(|uri| uri.to_string())
.collect();
assert_eq!(iface_uris, ["iface://v4.lo:8080/"]);
assert!(inet_pattern.interface_bind_uris("lo").next().is_none());
}
#[test]
fn unknown_explicit_scheme_falls_back_to_iface() {
let pattern: BindPattern = "custom://v4.en*:8080/path?query".parse().unwrap();
assert_eq!(pattern.scheme, Scheme::Iface);
assert_eq!(pattern.path_and_query_str(), Some("/path?query"));
assert_eq!(pattern.to_string(), "iface://v4.en*:8080/path?query");
}
}
impl fmt::Display for BindPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}://", self.scheme)?;
if let Some(family) = self.host.family() {
let tag = match family {
Family::V4 => "v4",
Family::V6 => "v6",
};
write!(f, "{tag}.")?;
}
if self.host.as_ip_addr().is_some_and(|ip| ip.is_ipv6()) {
write!(f, "[{}]", self.host)?;
} else {
write!(f, "{}", self.host)?;
}
if let Some(port) = self.port {
write!(f, ":{port}")?;
}
if let Some(ref pq) = self.path_and_query {
write!(f, "{pq}")?;
}
Ok(())
}
}
peg::parser! {
grammar bind_parser() for str {
rule family() -> Family
= "v4" { Family::V4 }
/ "V4" { Family::V4 }
/ "v6" { Family::V6 }
/ "V6" { Family::V6 }
rule scheme() -> &'input str
= s:$(['a'..='z' | 'A'..='Z']+) "://" { s }
rule port() -> u16
= ":" n:$(['0'..='9']+) {?
n.parse().or(Err("valid port number"))
}
rule host_char() -> char
= c:[^ ':' | '/' | '?' | '#' | '[' | ']'] { c }
rule bracket_segment()
= "[" [^ ']']+ "]"
rule host_token()
= bracket_segment()
/ host_char()
rule host_str() -> &'input str
= s:$(host_token()+) { s }
rule path_and_query() -> &'input str
= s:$(['/' | '?'] [_]*) { s }
pub rule full() -> BindPattern
= s:scheme()
fam:(f:family() "." { f })?
h:host_str()
p:port()?
pq:path_and_query()?
{?
let host = BindHost::classify(h, fam)?;
let scheme = infer_scheme(Some(s), &host);
let path_and_query = pq
.map(|s| s.parse::<PathAndQuery>())
.transpose()
.map_err(|_| "valid path-and-query")?;
Ok(BindPattern { scheme, host, port: p, path_and_query })
}
pub rule no_scheme() -> BindPattern
= fam:(f:family() "." { f })?
h:host_str()
p:port()?
pq:path_and_query()?
{?
let host = BindHost::classify(h, fam)?;
let scheme = infer_scheme(None, &host);
let path_and_query = pq
.map(|s| s.parse::<PathAndQuery>())
.transpose()
.map_err(|_| "valid path-and-query")?;
Ok(BindPattern { scheme, host, port: p, path_and_query })
}
pub rule bind() -> BindPattern
= b:bare_ip() { b }
/ b:full() { b }
/ b:no_scheme() { b }
rule bare_ip() -> BindPattern
= s:$([^ '/' | '?' | '#']+) pq:path_and_query()? {?
let addr = s.parse::<std::net::IpAddr>().or(Err("valid IP address"))?;
let path_and_query = pq
.map(|s| s.parse::<PathAndQuery>())
.transpose()
.map_err(|_| "valid path-and-query")?;
Ok(BindPattern {
scheme: Scheme::Inet,
host: BindHost::Ip { addr, repr: s.to_owned() },
port: None,
path_and_query,
})
}
}
}
fn infer_scheme(explicit: Option<&str>, host: &BindHost) -> Scheme {
if let Some(s) = explicit {
return match s.to_ascii_lowercase().as_str() {
"iface" => Scheme::Iface,
"inet" => Scheme::Inet,
_ => Scheme::Iface,
};
}
if host.is_ip_addr() {
Scheme::Inet
} else {
Scheme::Iface
}
}
impl FromStr for BindPattern {
type Err = ParseError<LineCol>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
bind_parser::bind(s)
}
}
impl BindPattern {
#[must_use]
pub fn path_and_query_str(&self) -> Option<&str> {
self.path_and_query.as_ref().map(|pq| pq.as_str())
}
#[must_use]
pub fn effective_port(&self) -> u16 {
self.port.unwrap_or(0)
}
#[must_use]
pub fn matches(&self, bind_uri: &BindUri) -> bool {
if self.scheme != bind_uri.scheme() {
return false;
}
match self.scheme {
Scheme::Iface => self.matches_iface_bind_uri(bind_uri),
Scheme::Inet => self.matches_inet_bind_uri(bind_uri),
_ => false,
}
}
pub(crate) fn interface_bind_uris<'a>(
&'a self,
interface: &'a str,
) -> impl Iterator<Item = BindUri> + use<'a> {
let template = self.template();
match self.scheme {
Scheme::Iface => {
Either::Left(self.match_interface_links(interface).filter_map(template))
}
_ => Either::Right(std::iter::empty()),
}
}
fn port_matches(&self, actual: u16) -> bool {
if let Some(expected) = self.port
&& expected != actual
{
return false;
}
true
}
fn matches_iface_bind_uri(&self, bind_uri: &BindUri) -> bool {
let Some((family, interface, port)) = bind_uri.as_iface_bind_uri() else {
return false;
};
if !self.port_matches(port) {
return false;
}
match &self.host {
BindHost::Ip { .. } => false,
host => {
if let Some(pattern_family) = host.family()
&& pattern_family != family
{
return false;
}
host.matches(interface)
}
}
}
fn matches_inet_bind_uri(&self, bind_uri: &BindUri) -> bool {
let Some(addr) = bind_uri.as_inet_bind_uri() else {
return false;
};
if !self.port_matches(addr.port()) {
return false;
}
match &self.host {
BindHost::Ip { addr: pattern, .. } => *pattern == addr.ip(),
BindHost::Glob { .. } | BindHost::Exact { .. } => false,
}
}
pub(crate) fn template(&self) -> impl Fn(Authority) -> Option<BindUri> + use<> {
let mut uri_template = Uri::from_static("iface://v4.lo:0/").into_parts();
uri_template.scheme = Some(self.scheme.into());
uri_template.path_and_query =
(self.path_and_query.clone()).or(uri_template.path_and_query.clone());
let uri_template = Uri::from_parts(uri_template)
.expect("BUG: bind URI template built from valid scheme and path-and-query");
move |authority: Authority| {
let mut uri_parts = uri_template.clone().into_parts();
uri_parts.authority = Some(authority);
let bind_uri =
(Uri::from_parts(uri_parts).ok()).and_then(|uri| BindUri::try_from(uri).ok())?;
Some(bind_uri)
}
}
pub(crate) fn match_interface_links(&self, interface: &str) -> impl Iterator<Item = Authority> {
match &self.host {
BindHost::Ip { .. } => Either::Left(std::iter::empty()),
host if !host.matches(interface) => Either::Left(std::iter::empty()),
host => Either::Right(host.families().iter().filter_map(move |family| {
format!("{family}.{interface}:{port}", port = self.effective_port())
.parse()
.ok()
})),
}
}
pub fn to_bind_uris<'a, I>(
&'a self,
interfaces: I,
) -> impl Iterator<Item = BindUri> + use<'a, I>
where
I: IntoIterator<Item = &'a str>,
{
let template = LazyCell::new(|| self.template());
let port = self.effective_port();
match &self.host {
BindHost::Ip { addr, .. } => {
let link: Authority = if addr.is_ipv6() {
format!("[{addr}]:{port}")
} else {
format!("{addr}:{port}")
}
.parse()
.expect("BUG: formatted IP address and port is a valid authority");
Either::Left(template(link).into_iter())
}
#[allow(clippy::redundant_closure)]
BindHost::Glob { .. } | BindHost::Exact { .. } => Either::Right(
interfaces
.into_iter()
.flat_map(move |iface| self.match_interface_links(iface))
.flat_map(move |link| template(link)),
),
}
}
}