"""Network utilities.""" from __future__ import annotations from ipaddress import IPv4Address, IPv6Address, ip_address, ip_network import re import yarl # RFC6890 - IP addresses of loopback interfaces IPV6_IPV4_LOOPBACK = ip_network("::ffff:127.0.0.0/104") LOOPBACK_NETWORKS = ( ip_network("127.0.0.0/8"), ip_network("::1/128"), IPV6_IPV4_LOOPBACK, ) # RFC6890 - Address allocation for Private Internets PRIVATE_NETWORKS = ( ip_network("10.0.0.0/8"), ip_network("172.16.0.0/12"), ip_network("192.168.0.0/16"), ip_network("fd00::/8"), ip_network("::ffff:10.0.0.0/104"), ip_network("::ffff:172.16.0.0/108"), ip_network("::ffff:192.168.0.0/112"), ) # RFC6890 - Link local ranges LINK_LOCAL_NETWORKS = ( ip_network("169.254.0.0/16"), ip_network("fe80::/10"), ip_network("::ffff:169.254.0.0/112"), ) def is_loopback(address: IPv4Address | IPv6Address) -> bool: """Check if an address is a loopback address.""" return address.is_loopback or address in IPV6_IPV4_LOOPBACK def is_private(address: IPv4Address | IPv6Address) -> bool: """Check if an address is a unique local non-loopback address.""" return any(address in network for network in PRIVATE_NETWORKS) def is_link_local(address: IPv4Address | IPv6Address) -> bool: """Check if an address is link-local (local but not necessarily unique).""" return address.is_link_local def is_local(address: IPv4Address | IPv6Address) -> bool: """Check if an address is on a local network.""" return is_loopback(address) or is_private(address) or is_link_local(address) def is_invalid(address: IPv4Address | IPv6Address) -> bool: """Check if an address is invalid.""" return address.is_unspecified def is_ip_address(address: str) -> bool: """Check if a given string is an IP address.""" try: ip_address(address) except ValueError: return False return True def is_ipv4_address(address: str) -> bool: """Check if a given string is an IPv4 address.""" try: IPv4Address(address) except ValueError: return False return True def is_ipv6_address(address: str) -> bool: """Check if a given string is an IPv6 address.""" try: IPv6Address(address) except ValueError: return False return True def is_host_valid(host: str) -> bool: """Check if a given string is an IP address or valid hostname.""" if is_ip_address(host): return True if len(host) > 255: return False if re.match(r"^[0-9\.]+$", host): # reject invalid IPv4 return False if host.endswith("."): # dot at the end is correct host = host[:-1] allowed = re.compile(r"(?!-)[A-Z\d\-]{1,63}(? str: """Normalize a given URL.""" url = yarl.URL(address.rstrip("/")) if url.is_absolute() and url.is_default_port(): return str(url.with_port(None)) return str(url)