use std::net::{SocketAddr, SocketAddrV4, SocketAddrV6, Ipv4Addr, Ipv6Addr};
pub struct PeerAddressValidator;
impl PeerAddressValidator {
pub fn validate_address(address: &str) -> Result<PeerAddress, AddressError> {
if let Ok(socket_addr) = address.parse::<SocketAddr>() {
return Ok(PeerAddress::Socket(socket_addr));
}
if address.ends_with(".dark") {
return Self::validate_dark_address(address);
}
if address.contains(".onion") {
return Self::validate_onion_address(address);
}
if let Some((domain, port_str)) = address.rsplit_once(':') {
if let Ok(port) = port_str.parse::<u16>() {
if port > 0 && Self::is_valid_domain(domain) {
return Ok(PeerAddress::Domain(domain.to_string(), port));
}
}
}
Err(AddressError::InvalidFormat)
}
fn validate_dark_address(address: &str) -> Result<PeerAddress, AddressError> {
if !address.ends_with(".dark") {
return Err(AddressError::InvalidDarkAddress);
}
let domain = address.strip_suffix(".dark").unwrap();
if domain.is_empty() || domain.len() > 63 {
return Err(AddressError::InvalidDarkAddress);
}
if !domain.chars().next().unwrap_or('0').is_ascii_lowercase() {
return Err(AddressError::InvalidDarkAddress);
}
for ch in domain.chars() {
if !ch.is_ascii_alphanumeric() && ch != '-' {
return Err(AddressError::InvalidDarkAddress);
}
}
Ok(PeerAddress::Dark(address.to_string()))
}
fn validate_onion_address(address: &str) -> Result<PeerAddress, AddressError> {
if let Some((onion_part, port_str)) = address.rsplit_once(':') {
let port = port_str.parse::<u16>()
.map_err(|_| AddressError::InvalidOnionAddress)?;
if port == 0 {
return Err(AddressError::InvalidOnionAddress);
}
if !onion_part.ends_with(".onion") {
return Err(AddressError::InvalidOnionAddress);
}
let onion_hash = onion_part.strip_suffix(".onion").unwrap();
match onion_hash.len() {
16 => {
for ch in onion_hash.chars() {
if !"abcdefghijklmnopqrstuvwxyz234567".contains(ch) {
return Err(AddressError::InvalidOnionAddress);
}
}
}
56 => {
for ch in onion_hash.chars() {
if !"abcdefghijklmnopqrstuvwxyz234567".contains(ch) {
return Err(AddressError::InvalidOnionAddress);
}
}
}
_ => return Err(AddressError::InvalidOnionAddress),
}
Ok(PeerAddress::Onion(address.to_string()))
} else {
Err(AddressError::InvalidOnionAddress)
}
}
fn is_valid_domain(domain: &str) -> bool {
if domain.is_empty() || domain.len() > 253 {
return false;
}
let parts: Vec<&str> = domain.split('.').collect();
if parts.len() < 2 {
return false;
}
for part in parts {
if part.is_empty() || part.len() > 63 {
return false;
}
if !part.chars().next().unwrap_or('0').is_ascii_alphanumeric() ||
!part.chars().last().unwrap_or('0').is_ascii_alphanumeric() {
return false;
}
for ch in part.chars() {
if !ch.is_ascii_alphanumeric() && ch != '-' {
return false;
}
}
}
true
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PeerAddress {
Socket(SocketAddr),
Domain(String, u16),
Dark(String),
Onion(String),
}
#[derive(Debug, Clone, PartialEq)]
pub enum AddressError {
InvalidFormat,
InvalidDarkAddress,
InvalidOnionAddress,
InvalidPort,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ipv4_address_validation() {
let cases = vec![
("127.0.0.1:8000", true),
("192.168.1.100:9000", true),
("10.0.0.1:1234", true),
("255.255.255.255:65535", true),
("0.0.0.0:1", true),
];
for (address, should_be_valid) in cases {
let result = PeerAddressValidator::validate_address(address);
if should_be_valid {
assert!(result.is_ok(), "Address {} should be valid", address);
if let Ok(PeerAddress::Socket(socket_addr)) = result {
assert_eq!(socket_addr.to_string(), address);
}
} else {
assert!(result.is_err(), "Address {} should be invalid", address);
}
}
}
#[test]
fn test_ipv6_address_validation() {
let cases = vec![
("[::1]:8000", true),
("[2001:db8::1]:9000", true),
("[fe80::1%lo0]:8000", false), ("[2001:db8:85a3::8a2e:370:7334]:443", true),
("[::]:80", true),
];
for (address, should_be_valid) in cases {
let result = PeerAddressValidator::validate_address(address);
if should_be_valid {
assert!(result.is_ok(), "Address {} should be valid", address);
if let Ok(PeerAddress::Socket(_)) = result {
}
} else {
assert!(result.is_err(), "Address {} should be invalid", address);
}
}
}
#[test]
fn test_domain_address_validation() {
let cases = vec![
("example.com:8000", true),
("node1.qudag.network:9000", true),
("test-node.example.org:443", true),
("localhost:8080", true),
("sub.domain.example.io:3000", true),
("a.b:1", true),
("invalid:0", false), ("invalid:", false), (":8000", false), ("invalid-domain-name-that-is-way-too-long-to-be-valid.com:8000", false),
("example..com:8000", false), ("-example.com:8000", false), ("example-.com:8000", false), ];
for (address, should_be_valid) in cases {
let result = PeerAddressValidator::validate_address(address);
if should_be_valid {
assert!(result.is_ok(), "Address {} should be valid", address);
if let Ok(PeerAddress::Domain(domain, port)) = result {
assert!(port > 0);
assert!(!domain.is_empty());
}
} else {
assert!(result.is_err(), "Address {} should be invalid", address);
}
}
}
#[test]
fn test_dark_address_validation() {
let cases = vec![
("mynode.dark", true),
("test-node.dark", true),
("a.dark", true),
("valid123.dark", true),
("node-with-dashes.dark", true),
(".dark", false), ("123node.dark", false), ("-node.dark", false), ("node-.dark", false), ("node.with.dots.dark", false), ("node_with_underscores.dark", false), ("NODE.dark", false), ("node.DARK", false), ("node.dark:8000", false), ("", false), ("verylongnodename0123456789012345678901234567890123456789012345.dark", false), ];
for (address, should_be_valid) in cases {
let result = PeerAddressValidator::validate_address(address);
if should_be_valid {
assert!(result.is_ok(), "Dark address {} should be valid", address);
if let Ok(PeerAddress::Dark(dark_addr)) = result {
assert_eq!(dark_addr, address);
assert!(dark_addr.ends_with(".dark"));
}
} else {
assert!(result.is_err(), "Dark address {} should be invalid", address);
}
}
}
#[test]
fn test_onion_address_validation() {
let cases = vec![
("3g2upl4pq6kufc4m.onion:8000", true),
("facebookcorewwwi.onion:443", true),
("duckduckgogg42ts.onion:80", true),
("facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion:443", true),
("3g2upl4pq6kufc4m236nokrhwwupnj7jqfkfxbh6kp5g6cjqzpgczjnqd.onion:8000", true),
("invalid.onion:8000", false), ("3g2upl4pq6kufc4m.onion:0", false), ("3g2upl4pq6kufc4m.onion", false), ("3G2UPL4PQ6KUFC4M.onion:8000", false), ("3g2upl4pq6kufc4m.onion:99999", false), ("3g2upl4pq6kufc4m.union:8000", false), ("", false), ];
for (address, should_be_valid) in cases {
let result = PeerAddressValidator::validate_address(address);
if should_be_valid {
assert!(result.is_ok(), "Onion address {} should be valid", address);
if let Ok(PeerAddress::Onion(onion_addr)) = result {
assert_eq!(onion_addr, address);
}
} else {
assert!(result.is_err(), "Onion address {} should be invalid", address);
}
}
}
#[test]
fn test_invalid_address_formats() {
let invalid_addresses = vec![
"",
"just-a-string",
"192.168.1.256:8000", "192.168.1.1:99999", "192.168.1.1:-1", "192.168.1.1:abc", "[invalid-ipv6]:8000",
"192.168.1.1", ":8000", "192.168.1.1:", "example.com::", "256.256.256.256:8000", ];
for address in invalid_addresses {
let result = PeerAddressValidator::validate_address(address);
assert!(result.is_err(), "Address {} should be invalid", address);
}
}
#[test]
fn test_port_range_validation() {
for port in [1, 80, 443, 8000, 9000, 65535] {
let address = format!("127.0.0.1:{}", port);
let result = PeerAddressValidator::validate_address(&address);
assert!(result.is_ok(), "Port {} should be valid", port);
}
let result = PeerAddressValidator::validate_address("example.com:0");
assert!(result.is_err(), "Port 0 should be invalid for domain addresses");
}
#[test]
fn test_address_type_detection() {
let test_cases = vec![
("127.0.0.1:8000", "Socket"),
("[::1]:8000", "Socket"),
("example.com:8000", "Domain"),
("mynode.dark", "Dark"),
("3g2upl4pq6kufc4m.onion:8000", "Onion"),
];
for (address, expected_type) in test_cases {
let result = PeerAddressValidator::validate_address(address);
assert!(result.is_ok(), "Address {} should be valid", address);
let actual_type = match result.unwrap() {
PeerAddress::Socket(_) => "Socket",
PeerAddress::Domain(_, _) => "Domain",
PeerAddress::Dark(_) => "Dark",
PeerAddress::Onion(_) => "Onion",
};
assert_eq!(actual_type, expected_type,
"Address {} should be detected as {} type", address, expected_type);
}
}
#[test]
fn test_edge_cases() {
let long_domain = "a".repeat(50) + ".com:8000";
let result = PeerAddressValidator::validate_address(&long_domain);
assert!(result.is_ok(), "Long valid domain should be accepted");
let max_port_addr = "127.0.0.1:65535";
let result = PeerAddressValidator::validate_address(max_port_addr);
assert!(result.is_ok(), "Maximum port should be valid");
let min_port_addr = "127.0.0.1:1";
let result = PeerAddressValidator::validate_address(min_port_addr);
assert!(result.is_ok(), "Minimum port should be valid");
}
#[test]
fn test_normalization() {
let address = "Example.COM:8000";
let result = PeerAddressValidator::validate_address(address);
assert!(result.is_err(), "Uppercase domains should be rejected");
}
}