use anyhow::Result;
use hickory_proto::{rr::domain::Name, xfer::Protocol};
use hickory_resolver::{
Resolver, config::NameServerConfig, config::ResolverConfig,
name_server::TokioConnectionProvider,
};
use ip2asn::{Builder, IpAsnMap};
use std::{
env,
fs::File,
io::{self, BufRead},
net::{IpAddr, SocketAddr},
path::Path,
str::FromStr,
};
use tracing::{Level, event};
fn is_tmp_file_exists(filename: &str) -> bool {
let dir = env::temp_dir();
Path::new(dir.join(filename).as_os_str()).exists()
}
async fn fetch_and_save_asn_db(url: &str, path: &Path) -> Result<()> {
let response = reqwest::get(url).await?.bytes().await?;
let mut dest = File::create(path)?;
io::copy(&mut response.as_ref(), &mut dest)
.map_err(|e| anyhow::anyhow!("Failed to save ASN database: {}", e))?;
event!(Level::INFO, "Downloaded ASN database to {}", path.display());
Ok(())
}
pub async fn open_asn_db() -> Result<IpAsnMap> {
let filename = "ip2asn-combined.tsv.gz";
let url = "https://iptoasn.com/data/ip2asn-combined.tsv.gz";
let dir = env::temp_dir();
let path = dir.join(filename);
if !is_tmp_file_exists(filename) {
fetch_and_save_asn_db(url, &path).await.map_err(|e| {
anyhow::anyhow!(
"Failed to fetch ASN database from {}: {}",
url,
e.to_string()
)
})?;
event!(Level::INFO, "ASN database fetched successfully.");
}
event!(Level::INFO, "Loading ASN database from {}", path.display());
let ipasn = Builder::new().from_path(path)?.build()?;
Ok(ipasn)
}
pub fn parse_ip_list(ip_list: &str) -> Vec<IpAddr> {
ip_list
.split(',')
.filter_map(|s| s.trim().parse::<IpAddr>().ok())
.collect()
}
pub fn get_dns_config_from_ips(dns_ips: &[IpAddr]) -> Vec<NameServerConfig> {
dns_ips
.iter()
.map(|&ip| {
let socket_addr = SocketAddr::new(ip, 53);
NameServerConfig::new(socket_addr, Protocol::Udp)
})
.collect()
}
pub fn get_default_dns_config() -> Result<Resolver<TokioConnectionProvider>> {
let ip: IpAddr = "1.1.1.1".parse()?;
let socket_addr = SocketAddr::new(ip, 53);
let name_server_config = NameServerConfig::new(socket_addr, Protocol::Udp);
let name = Name::from_str("luxbulb.org.")?;
let resolver_config = ResolverConfig::from_parts(Some(name), vec![], vec![name_server_config]);
Ok(Resolver::builder_with_config(resolver_config, TokioConnectionProvider::default()).build())
}
pub fn get_resolver(custom_dns: Option<String>) -> Result<Resolver<TokioConnectionProvider>> {
if let Some(custom_dns) = custom_dns {
let dns_ips = parse_ip_list(&custom_dns);
if !dns_ips.is_empty() {
event!(
Level::INFO,
"Resolution using custom DNS servers: {:?}",
dns_ips
);
let dns_config = get_dns_config_from_ips(&dns_ips);
let name = Name::from_str("luxbulb.org.")?;
let resolver_config = ResolverConfig::from_parts(Some(name), vec![], dns_config);
Ok(
Resolver::builder_with_config(resolver_config, TokioConnectionProvider::default())
.build(),
)
} else {
event!(Level::INFO, "Resolution using default DNS servers: 1.1.1.1");
get_default_dns_config()
}
} else {
event!(Level::INFO, "Resolution using default DNS servers: 1.1.1.1");
get_default_dns_config()
}
}
pub fn chunked<I>(
a: impl IntoIterator<Item = I>,
chunk_size: usize,
) -> impl Iterator<Item = Vec<I>> {
let mut a = a.into_iter();
std::iter::from_fn(move || {
Some(a.by_ref().take(chunk_size).collect()).filter(|chunk: &Vec<_>| !chunk.is_empty())
})
}
pub fn count_lines(path: &str) -> Result<usize> {
let file = File::open(path).map_err(|e| anyhow::anyhow!("Failed to open CSV file: {}", e))?;
let mut lines = std::io::BufReader::new(file).lines();
let count = lines.try_fold(0, |acc, line| line.map(|_| acc + 1))?;
Ok(count)
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
use std::net::SocketAddr;
#[test]
fn test_chunked() {
let data = vec![1, 2, 3, 4, 5, 6, 7];
let chunk_size = 3;
let chunks: Vec<Vec<i32>> = chunked(data, chunk_size).collect();
assert_eq!(chunks, vec![vec![1, 2, 3], vec![4, 5, 6], vec![7]]);
}
#[tokio::test]
async fn test_open_asn_db() {
let result_fetch = open_asn_db().await;
assert!(result_fetch.is_ok());
let result_tmp = open_asn_db().await;
assert!(result_tmp.is_ok());
}
#[tokio::test]
async fn test_fetch_and_save_asn_db() {
let filename = "test_ip2asn-combined.tsv.gz";
let url = "https://iptoasn.com/data/ip2asn-combined.tsv.gz";
let dir = env::temp_dir();
let path = dir.join(filename);
if is_tmp_file_exists(filename) {
std::fs::remove_file(&path).unwrap();
}
let result = fetch_and_save_asn_db(url, &path).await;
assert!(result.is_ok());
assert!(is_tmp_file_exists(filename));
std::fs::remove_file(&path).unwrap();
}
#[tokio::test]
async fn test_get_resolver() {
let resolver = get_resolver(None).unwrap();
assert_eq!(
resolver.config().name_servers()[0].socket_addr,
SocketAddr::from(([1, 1, 1, 1], 53))
);
let response = resolver.lookup_ip("example.com").await;
assert!(response.is_ok());
}
#[test]
fn test_parse_ip_list() {
let ip_list = "1.1.1.1, 8.8.8.8, 8.8.4.4";
let parsed_ips = parse_ip_list(ip_list);
assert_eq!(parsed_ips.len(), 3);
assert_eq!(parsed_ips[0], Ipv4Addr::new(1, 1, 1, 1));
assert_eq!(parsed_ips[1], Ipv4Addr::new(8, 8, 8, 8));
assert_eq!(parsed_ips[2], Ipv4Addr::new(8, 8, 4, 4));
}
#[test]
fn test_parse_ip_list_with_error() {
let ip_list = "1.1.";
let parsed_ips = parse_ip_list(ip_list);
assert_eq!(parsed_ips.len(), 0);
}
#[test]
fn test_count_lines() {
let test_file_path = "./data/test-10k.csv";
let line_count = count_lines(test_file_path).unwrap();
assert_eq!(line_count, 10000);
}
}