use dashmap::DashMap;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::time::{Duration, Instant};
const DNS_CACHE_TTL: Duration = Duration::from_secs(60);
const DNS_CACHE_MAX_ENTRIES: usize = 4096;
type DnsCacheEntry = (Instant, Arc<Vec<SocketAddr>>);
static DNS_CACHE: std::sync::OnceLock<DashMap<String, DnsCacheEntry>> = std::sync::OnceLock::new();
pub async fn resolve_dns_cached(host_port: &str) -> std::io::Result<Vec<SocketAddr>> {
let cache = DNS_CACHE.get_or_init(DashMap::new);
if let Some(entry) = cache.get(host_port) {
let (inserted_at, addrs) = entry.value();
if inserted_at.elapsed() < DNS_CACHE_TTL {
return Ok((**addrs).clone());
}
drop(entry);
cache.remove(host_port);
}
let addrs: Vec<SocketAddr> = tokio::net::lookup_host(host_port).await?.collect();
if !addrs.is_empty() {
if cache.len() >= DNS_CACHE_MAX_ENTRIES {
crate::cache::evict_oldest_dashmap_entries(
cache,
crate::cache::oldest_eviction_batch(DNS_CACHE_MAX_ENTRIES),
|(inserted_at, _)| *inserted_at,
);
}
cache.insert(
host_port.to_string(),
(Instant::now(), Arc::new(addrs.clone())),
);
}
Ok(addrs)
}
#[inline]
fn verifier_blocks_ip_addr(ip: IpAddr) -> bool {
if crate::bogon::ip_addr_is_bogon(ip) {
return true;
}
match ip {
IpAddr::V4(ipv4) => ipv4.is_multicast() || ipv4.octets()[0] >= 240,
IpAddr::V6(_) => false,
}
}
#[inline]
pub fn is_private_ip_addr_fast(ip: &IpAddr) -> bool {
verifier_blocks_ip_addr(*ip)
}
#[inline]
pub fn is_private_ip_addr(ip: &IpAddr) -> bool {
verifier_blocks_ip_addr(*ip)
}
pub fn is_private_url(url_str: &str) -> bool {
let url = match url::Url::parse(url_str) {
Ok(u) => u,
Err(_) => return true, };
if !matches!(url.scheme(), "http" | "https") {
return true; }
let Some(host) = url.host() else {
return true; };
match host {
url::Host::Ipv4(ip) => {
if verifier_blocks_ip_addr(IpAddr::V4(ip)) {
return true;
}
}
url::Host::Ipv6(ip) => {
if verifier_blocks_ip_addr(IpAddr::V6(ip)) {
return true;
}
}
url::Host::Domain(d) => {
if !d.contains('.')
|| d == "localhost"
|| d.ends_with(".localhost")
|| d.ends_with(".local")
|| d.ends_with(".internal")
|| d.ends_with(".localdomain")
{
return true;
}
let maybe_ip = if d.contains('.') {
match d.parse::<Ipv4Addr>() {
Ok(address) => Some(address),
Err(_not_standard_ipv4) => canonicalize_short_form_ipv4(d),
}
} else if let Some(hex) = d.strip_prefix("0x").or_else(|| d.strip_prefix("0X")) {
u32::from_str_radix(hex, 16).ok().map(Ipv4Addr::from) } else if d.starts_with('0') && d.len() > 1 && d.chars().all(|c| c.is_ascii_digit()) {
u32::from_str_radix(d, 8).ok().map(Ipv4Addr::from) } else {
d.parse::<u32>().ok().map(Ipv4Addr::from) };
if let Some(ip) = maybe_ip {
if verifier_blocks_ip_addr(IpAddr::V4(ip)) {
return true;
}
}
if looks_like_malformed_ip(d) {
return true;
}
}
}
false
}
fn canonicalize_short_form_ipv4(domain: &str) -> Option<Ipv4Addr> {
let mut values = [0u32; 3];
let mut len = 0usize;
for part in domain.split('.') {
if len == values.len() {
return None;
}
values[len] = parse_ip_field(part)?;
len += 1;
}
if len < 2 {
return None;
}
let mut acc: u32 = 0;
for &leading in &values[..len - 1] {
if leading > 0xFF {
return None;
}
acc = (acc << 8) | leading;
}
let remaining_bytes = 4 - (len - 1);
let last = values[len - 1];
let max_last = if remaining_bytes >= 4 {
u32::MAX
} else {
(1u32 << (8 * remaining_bytes as u32)) - 1
};
if last > max_last {
return None;
}
acc = (acc << (8 * remaining_bytes as u32)) | last;
Some(Ipv4Addr::from(acc))
}
fn parse_ip_field(part: &str) -> Option<u32> {
if part.is_empty() {
return None;
}
if let Some(hex) = part.strip_prefix("0x").or_else(|| part.strip_prefix("0X")) {
if hex.is_empty() {
return None;
}
u32::from_str_radix(hex, 16).ok() } else if part.len() > 1 && part.starts_with('0') {
u32::from_str_radix(part, 8).ok() } else {
part.parse::<u32>().ok() }
}
fn looks_like_malformed_ip(domain: &str) -> bool {
let mut part_count = 0usize;
let mut all_octet_shaped = true;
let mut all_octal_shaped = true;
for part in domain.split('.') {
part_count += 1;
if part.is_empty() {
all_octet_shaped = false;
all_octal_shaped = false;
continue;
}
if !part
.chars()
.all(|c| c.is_ascii_hexdigit() || c == '-' || c == 'x' || c == 'X')
{
all_octet_shaped = false;
}
if !(part.starts_with('0') && part.len() > 1 && part.chars().all(|c| c.is_ascii_digit())) {
all_octal_shaped = false;
}
}
if part_count >= 4 && all_octet_shaped {
return true;
}
if part_count == 4 && all_octal_shaped {
return true;
}
false
}