//! Shared network endpoint parsing for LAN party tunnel binaries. use std::{ fmt, net::{IpAddr, Ipv6Addr, SocketAddr, ToSocketAddrs}, str::FromStr, }; use thiserror::Error; pub const DEFAULT_RELAY_PORT: u16 = 443; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct RelayEndpoint { host: String, port: u16, } impl RelayEndpoint { pub fn new(host: impl Into, port: u16) -> Result { let host = host.into(); let host = host.trim(); if host.is_empty() { return Err(RelayEndpointError::EmptyHost); } if port == 0 { return Err(RelayEndpointError::InvalidPort { value: port.to_string(), }); } Ok(Self { host: host.to_owned(), port, }) } #[must_use] pub fn host(&self) -> &str { &self.host } #[must_use] pub const fn port(&self) -> u16 { self.port } pub fn resolve(&self) -> Result { let mut addrs = (self.host.as_str(), self.port) .to_socket_addrs() .map_err(|source| RelayEndpointError::ResolveFailed { endpoint: self.clone(), source, })?; addrs .next() .ok_or_else(|| RelayEndpointError::NoResolvedAddress { endpoint: self.clone(), }) } } impl fmt::Display for RelayEndpoint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.host.parse::().is_ok() || self.host.contains(':') { write!(f, "[{}]:{}", self.host, self.port) } else { write!(f, "{}:{}", self.host, self.port) } } } impl FromStr for RelayEndpoint { type Err = RelayEndpointError; fn from_str(value: &str) -> Result { let value = value.trim(); if value.is_empty() { return Err(RelayEndpointError::Empty); } if let Ok(socket_addr) = value.parse::() { return Self::new(socket_addr.ip().to_string(), socket_addr.port()); } if let Ok(ip_addr) = value.parse::() { return Self::new(ip_addr.to_string(), DEFAULT_RELAY_PORT); } if let Some(bracketed) = value.strip_prefix('[') && let Some((host, rest)) = bracketed.split_once(']') { let port = match rest.strip_prefix(':') { Some(port) => parse_port(port)?, None if rest.is_empty() => DEFAULT_RELAY_PORT, None => { return Err(RelayEndpointError::InvalidSyntax { value: value.to_owned(), }); } }; return Self::new(host, port); } if value.matches(':').count() == 1 { let (host, port) = value.rsplit_once(':').expect("one colon must split"); return Self::new(host, parse_port(port)?); } if value.contains(':') { return Err(RelayEndpointError::InvalidSyntax { value: value.to_owned(), }); } Self::new(value, DEFAULT_RELAY_PORT) } } fn parse_port(value: &str) -> Result { value .parse::() .ok() .filter(|port| *port != 0) .ok_or_else(|| RelayEndpointError::InvalidPort { value: value.to_owned(), }) } #[derive(Debug, Error)] pub enum RelayEndpointError { #[error("relay endpoint cannot be empty")] Empty, #[error("relay endpoint host cannot be empty")] EmptyHost, #[error("relay endpoint must be HOST, HOST:PORT, IP, or [IPv6]:PORT: {value}")] InvalidSyntax { value: String }, #[error("relay endpoint port must be between 1 and 65535: {value}")] InvalidPort { value: String }, #[error("relay endpoint did not resolve to any addresses: {endpoint}")] NoResolvedAddress { endpoint: RelayEndpoint }, #[error("failed to resolve relay endpoint {endpoint}: {source}")] ResolveFailed { endpoint: RelayEndpoint, source: std::io::Error, }, } #[cfg(test)] mod tests { use super::*; #[test] fn parses_host_with_default_relay_port() { let endpoint: RelayEndpoint = "relay.example.test".parse().unwrap(); assert_eq!(endpoint.host(), "relay.example.test"); assert_eq!(endpoint.port(), DEFAULT_RELAY_PORT); assert_eq!(endpoint.to_string(), "relay.example.test:443"); } #[test] fn parses_host_with_explicit_port() { let endpoint: RelayEndpoint = "relay.example.test:8443".parse().unwrap(); assert_eq!(endpoint.host(), "relay.example.test"); assert_eq!(endpoint.port(), 8443); assert_eq!(endpoint.to_string(), "relay.example.test:8443"); } #[test] fn parses_ip_literals() { let ipv4: RelayEndpoint = "203.0.113.10".parse().unwrap(); let ipv4_with_port: RelayEndpoint = "203.0.113.10:8443".parse().unwrap(); let ipv6: RelayEndpoint = "2001:db8::1".parse().unwrap(); let ipv6_ending_with_443: RelayEndpoint = "2001:db8::1:443".parse().unwrap(); let ipv6_with_port: RelayEndpoint = "[2001:db8::1]:8443".parse().unwrap(); assert_eq!(ipv4.host(), "203.0.113.10"); assert_eq!(ipv4.port(), DEFAULT_RELAY_PORT); assert_eq!(ipv4_with_port.host(), "203.0.113.10"); assert_eq!(ipv4_with_port.port(), 8443); assert_eq!(ipv6.host(), "2001:db8::1"); assert_eq!(ipv6.port(), DEFAULT_RELAY_PORT); assert_eq!(ipv6.to_string(), "[2001:db8::1]:443"); assert_eq!(ipv6_ending_with_443.host(), "2001:db8::1:443"); assert_eq!(ipv6_ending_with_443.port(), DEFAULT_RELAY_PORT); assert_eq!(ipv6_with_port.host(), "2001:db8::1"); assert_eq!(ipv6_with_port.port(), 8443); } #[test] fn resolves_ip_literal_without_dns() { let endpoint: RelayEndpoint = "127.0.0.1:443".parse().unwrap(); assert_eq!( endpoint.resolve().unwrap(), "127.0.0.1:443".parse::().unwrap() ); } #[test] fn rejects_invalid_relay_endpoints() { assert!("".parse::().is_err()); assert!(":443".parse::().is_err()); assert!("relay.example.test:0".parse::().is_err()); assert!( "relay.example.test:not-a-port" .parse::() .is_err() ); assert!("[2001:db8::1]extra".parse::().is_err()); assert!("relay:example:443".parse::().is_err()); } }