use proc_macro_regex::regex;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use std::fmt::{Display, Formatter};
use std::net::ToSocketAddrs;
use std::num::ParseIntError;
use std::str::FromStr;
use std::time::Duration;
use validator::{Validate, ValidationError};
use hopr_transport_identity::Multiaddr;
pub use hopr_transport_network::{config::NetworkConfig, heartbeat::HeartbeatConfig};
pub use hopr_transport_protocol::config::ProtocolConfig;
use crate::errors::HoprTransportError;
pub struct HoprTransportConfig {
pub transport: TransportConfig,
pub network: hopr_transport_network::config::NetworkConfig,
pub protocol: hopr_transport_protocol::config::ProtocolConfig,
pub heartbeat: hopr_transport_network::heartbeat::HeartbeatConfig,
pub session: SessionGlobalConfig,
}
regex!(is_dns_address_regex "^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)*[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$");
#[inline]
pub fn looks_like_domain(s: &str) -> bool {
is_dns_address_regex(s)
}
pub fn is_reachable_domain(host: &str) -> bool {
host.to_socket_addrs().is_ok_and(|i| i.into_iter().next().is_some())
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum HostType {
IPv4(String),
Domain(String),
}
impl Default for HostType {
fn default() -> Self {
HostType::IPv4("127.0.0.1".to_owned())
}
}
#[derive(Debug, Serialize, Deserialize, Validate, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct HostConfig {
#[serde(default)] pub address: HostType,
#[validate(range(min = 1u16))]
#[serde(default)] pub port: u16,
}
impl FromStr for HostConfig {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (ip_or_dns, str_port) = match s.split_once(':') {
None => return Err("Invalid host, is not in the '<host>:<port>' format".into()),
Some(split) => split,
};
let port = str_port.parse().map_err(|e: ParseIntError| e.to_string())?;
if validator::ValidateIp::validate_ipv4(&ip_or_dns) {
Ok(Self {
address: HostType::IPv4(ip_or_dns.to_owned()),
port,
})
} else if looks_like_domain(ip_or_dns) {
Ok(Self {
address: HostType::Domain(ip_or_dns.to_owned()),
port,
})
} else {
Err("Not a valid IPv4 or domain host".into())
}
}
}
impl Display for HostConfig {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}:{}", self.address, self.port)
}
}
#[cfg(not(feature = "transport-quic"))]
fn default_multiaddr_transport(port: u16) -> String {
format!("tcp/{port}")
}
#[cfg(feature = "transport-quic")]
fn default_multiaddr_transport(port: u16) -> String {
format!("udp/{port}/quic-v1")
}
impl TryFrom<&HostConfig> for Multiaddr {
type Error = HoprTransportError;
fn try_from(value: &HostConfig) -> Result<Self, Self::Error> {
match &value.address {
HostType::IPv4(ip) => Multiaddr::from_str(
format!("/ip4/{}/{}", ip.as_str(), default_multiaddr_transport(value.port)).as_str(),
)
.map_err(|e| HoprTransportError::Api(e.to_string())),
HostType::Domain(domain) => Multiaddr::from_str(
format!("/dns4/{}/{}", domain.as_str(), default_multiaddr_transport(value.port)).as_str(),
)
.map_err(|e| HoprTransportError::Api(e.to_string())),
}
}
}
fn validate_ipv4_address(s: &str) -> Result<(), ValidationError> {
if validator::ValidateIp::validate_ipv4(&s) {
let ipv4 = std::net::Ipv4Addr::from_str(s)
.map_err(|_| ValidationError::new("Failed to deserialize the string into an ipv4 address"))?;
if ipv4.is_private() || ipv4.is_multicast() || ipv4.is_unspecified() {
return Err(ValidationError::new(
"IPv4 cannot be private, multicast or unspecified (0.0.0.0)",
))?;
}
Ok(())
} else {
Err(ValidationError::new("Invalid IPv4 address provided"))
}
}
fn validate_dns_address(s: &str) -> Result<(), ValidationError> {
if looks_like_domain(s) || is_reachable_domain(s) {
Ok(())
} else {
Err(ValidationError::new("Invalid DNS address provided"))
}
}
pub fn validate_external_host(host: &HostConfig) -> Result<(), ValidationError> {
match &host.address {
HostType::IPv4(ip4) => validate_ipv4_address(ip4),
HostType::Domain(domain) => validate_dns_address(domain),
}
}
#[derive(Debug, Default, Serialize, Deserialize, Validate, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct TransportConfig {
#[serde(default)]
pub announce_local_addresses: bool,
#[serde(default)]
pub prefer_local_addresses: bool,
}
const DEFAULT_SESSION_IDLE_TIMEOUT: Duration = Duration::from_secs(180);
const SESSION_IDLE_MIN_TIMEOUT: Duration = Duration::from_secs(60);
const DEFAULT_SESSION_ESTABLISH_RETRY_DELAY: Duration = Duration::from_secs(2);
const DEFAULT_SESSION_ESTABLISH_MAX_RETRIES: u32 = 3;
fn default_session_establish_max_retries() -> u32 {
DEFAULT_SESSION_ESTABLISH_MAX_RETRIES
}
fn default_session_idle_timeout() -> std::time::Duration {
DEFAULT_SESSION_IDLE_TIMEOUT
}
fn default_session_establish_retry_delay() -> std::time::Duration {
DEFAULT_SESSION_ESTABLISH_RETRY_DELAY
}
fn validate_session_idle_timeout(value: &std::time::Duration) -> Result<(), ValidationError> {
if SESSION_IDLE_MIN_TIMEOUT <= *value {
Ok(())
} else {
Err(ValidationError::new("session idle timeout is too low"))
}
}
#[serde_as]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Validate, smart_default::SmartDefault)]
#[serde(deny_unknown_fields)]
pub struct SessionGlobalConfig {
#[validate(custom(function = "validate_session_idle_timeout"))]
#[default(DEFAULT_SESSION_IDLE_TIMEOUT)]
#[serde(default = "default_session_idle_timeout")]
#[serde_as(as = "serde_with::DurationSeconds<u64>")]
pub idle_timeout: std::time::Duration,
#[validate(range(min = 0, max = 20))]
#[default(DEFAULT_SESSION_ESTABLISH_MAX_RETRIES)]
#[serde(default = "default_session_establish_max_retries")]
pub establish_max_retries: u32,
#[default(DEFAULT_SESSION_ESTABLISH_RETRY_DELAY)]
#[serde(default = "default_session_establish_retry_delay")]
#[serde_as(as = "serde_with::DurationSeconds<u64>")]
pub establish_retry_timeout: std::time::Duration,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_domains_for_looks_like_a_domain() {
assert!(looks_like_domain("localhost"));
assert!(looks_like_domain("hoprnet.org"));
assert!(looks_like_domain("hub.hoprnet.org"));
}
#[test]
fn test_valid_domains_for_does_not_look_like_a_domain() {
assert!(!looks_like_domain(".org"));
assert!(!looks_like_domain("-hoprnet-.org"));
}
#[test]
fn test_valid_domains_should_be_reachable() {
assert!(!is_reachable_domain("google.com"));
}
#[test]
fn test_verify_valid_ip4_addresses() {
assert!(validate_ipv4_address("1.1.1.1").is_ok());
assert!(validate_ipv4_address("1.255.1.1").is_ok());
assert!(validate_ipv4_address("187.1.1.255").is_ok());
assert!(validate_ipv4_address("127.0.0.1").is_ok());
}
#[test]
fn test_verify_invalid_ip4_addresses() {
assert!(validate_ipv4_address("1.256.1.1").is_err());
assert!(validate_ipv4_address("-1.1.1.255").is_err());
assert!(validate_ipv4_address("127.0.0.256").is_err());
assert!(validate_ipv4_address("1").is_err());
assert!(validate_ipv4_address("1.1").is_err());
assert!(validate_ipv4_address("1.1.1").is_err());
assert!(validate_ipv4_address("1.1.1.1.1").is_err());
}
#[test]
fn test_verify_valid_dns_addresses() {
assert!(validate_dns_address("localhost").is_ok());
assert!(validate_dns_address("google.com").is_ok());
assert!(validate_dns_address("hub.hoprnet.org").is_ok());
}
#[test]
fn test_verify_invalid_dns_addresses() {
assert!(validate_dns_address("-hoprnet-.org").is_err());
}
}