1use std::{
2 fs::{self, File},
3 io::Write,
4 net::{IpAddr, SocketAddr},
5 time::Duration,
6};
7
8use anyhow::{bail, Error};
9use fastping_rs::Pinger;
10use log::info;
11
12use rayon::prelude::{IntoParallelRefMutIterator, ParallelIterator};
13use tabled::Tabled;
14use trust_dns_resolver::{
15 config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts},
16 Resolver,
17};
18use url::Url;
19
20#[derive(Debug)]
21pub struct Record {
22 domain: String,
23 addrs: Vec<IpAddr>,
24 latency: Duration,
25 geo: String,
26 err: Option<String>,
27}
28
29type GeoIpReader<'a> = &'a maxminddb::Reader<Vec<u8>>;
30
31impl Record {
32 pub fn to_tabled(&self) -> TabledRecord {
33 TabledRecord::from(self)
34 }
35
36 fn new(domain: String) -> Self {
37 Self {
38 domain,
39 addrs: vec![],
40 latency: Default::default(),
41 geo: "".to_string(),
42 err: None,
43 }
44 }
45
46 fn analysis(&mut self, resolver: &Resolver, geo_ip_reader: GeoIpReader) {
47 match self.lookup(resolver) {
48 Ok(addrs) => self.addrs = addrs,
49 Err(err) => {
50 self.err = Some(format!("{}", err));
51 return;
52 }
53 }
54
55 if !self.addrs.is_empty() {
56 match self.latency(*self.addrs.first().unwrap()) {
57 Ok(d) => self.latency = d,
58 Err(err) => {
59 self.err = Some(format!("{}", err));
60 }
61 }
62
63 if let Ok(addr) = self.geoip(geo_ip_reader, &self.addrs) {
64 self.geo = addr.join("\n");
65 }
66 }
67 }
68
69 fn lookup(&self, resolver: &Resolver) -> Result<Vec<IpAddr>, Error> {
70 let result = resolver.lookup_ip(&self.domain)?;
71 Ok(result.iter().collect::<Vec<_>>())
72 }
73
74 fn latency(&self, addr: IpAddr) -> Result<Duration, Error> {
75 let (pinger, results) = match Pinger::new(Some(800), Some(56)) {
76 Ok((pinger, results)) => (pinger, results),
77 Err(e) => panic!("Error creating pinger: {}", e),
78 };
79
80 pinger.add_ipaddr(&addr.to_string());
81
82 pinger.ping_once();
83
84 match results.recv() {
85 Ok(result) => match result {
86 fastping_rs::PingResult::Idle { addr: _ } => bail!("timeout"),
87 fastping_rs::PingResult::Receive { addr: _, rtt } => Ok(rtt),
88 },
89 Err(e) => bail!("{e}"),
90 }
91 }
92
93 fn geoip(&self, geo_ip_reader: GeoIpReader, addrs: &Vec<IpAddr>) -> Result<Vec<String>, Error> {
94 let mut buf = vec![];
95
96 for addr in addrs {
97 let mut v = vec![];
98 let r: maxminddb::geoip2::City = geo_ip_reader.lookup(*addr)?;
99 if let Some(country) = r.country {
100 v.push(country.names.unwrap().get("en").unwrap_or(&"").to_string());
101 }
102
103 if let Some(city) = r.city {
104 v.push(city.names.unwrap().get("en").unwrap_or(&"").to_string());
105 }
106
107 v.dedup();
108
109 buf.push(v.join(" / "));
110 }
111
112 Ok(buf)
113 }
114}
115
116pub fn analysis(file_path: &str, dns_server: Option<String>) -> Result<Vec<Record>, Error> {
117 let resolver = build_resolve(dns_server)?;
118 let reader = build_geoip_reader()?;
119
120 let domains = get_domains(file_path)?;
121 let mut records = build_records(domains);
122
123 records.par_iter_mut().for_each(|v| {
124 v.analysis(&resolver, &reader);
125 });
126
127 Ok(records)
128}
129
130fn build_resolve(dns_server: Option<String>) -> Result<Resolver, Error> {
131 let mut resolver = Resolver::from_system_conf()?;
132
133 if let Some(dns_server) = dns_server {
134 let mut ip_addr: &str = &dns_server;
135 let mut port = 53;
136 if let Some(i) = dns_server.find(':') {
137 ip_addr = &dns_server[..i];
138 port = dns_server[i + 1..].parse()?;
139 }
140 let addr = ip_addr.parse()?;
141
142 let socket_addr = SocketAddr::new(addr, port);
143 let name_server = NameServerConfig::new(socket_addr, Protocol::Udp);
144
145 let mut config = ResolverConfig::new();
146 config.add_name_server(name_server);
147
148 resolver = Resolver::new(config, ResolverOpts::default())?;
149 }
150
151 Ok(resolver)
152}
153
154fn build_geoip_reader() -> Result<maxminddb::Reader<Vec<u8>>, Error> {
155 let home = dirs::home_dir().unwrap();
156 let dir = home.join(".geolite2");
157 let file_path = dir.join("GeoLite2-City.mmdb");
158
159 if !file_path.exists() {
160 fs::create_dir_all(dir)?;
161 info!("downloading GeoLite2-City.mmdb");
162 let response = reqwest::blocking::get(
163 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-City.mmdb",
164 )?;
165 let mut file = File::create(file_path.clone())?;
166 let data = response.bytes()?.to_vec();
167 file.write_all(&data)?;
168 file.flush()?;
169 }
170
171 Ok(maxminddb::Reader::open_readfile(file_path)?)
172}
173
174fn get_domains(file_path: &str) -> Result<Vec<String>, Error> {
175 let har = har::from_path(file_path)?;
176
177 let urls = match har.log {
178 har::Spec::V1_2(l) => l
179 .entries
180 .iter()
181 .map(|v| v.request.url.clone())
182 .collect::<Vec<_>>(),
183 har::Spec::V1_3(l) => l
184 .entries
185 .iter()
186 .map(|v| v.request.url.clone())
187 .collect::<Vec<_>>(),
188 };
189
190 let mut domains = urls
191 .iter()
192 .map(|v| match Url::parse(v) {
193 Ok(v) => Some(String::from(v.host_str().unwrap())),
194 Err(_) => None,
195 })
196 .filter(|v| v.is_some())
197 .flatten()
198 .collect::<Vec<_>>();
199
200 domains.sort();
201 domains.dedup();
202
203 Ok(domains)
204}
205
206fn build_records(domains: Vec<String>) -> Vec<Record> {
207 domains
208 .iter()
209 .map(|v| Record::new(String::from(v)))
210 .collect()
211}
212
213#[derive(Tabled)]
214pub struct TabledRecord {
215 domain: String,
216 addrs: String,
217 latency: String,
218 geo: String,
219 err: String,
220}
221
222impl From<&Record> for TabledRecord {
223 fn from(r: &Record) -> Self {
224 let addrs = r
225 .addrs
226 .iter()
227 .map(|v| format!("{}", v))
228 .collect::<Vec<String>>()
229 .join("\n");
230
231 let latency = format!("{}ms", r.latency.as_millis());
232
233 Self {
234 domain: r.domain.clone(),
235 addrs,
236 latency,
237 geo: r.geo.clone(),
238 err: r.err.clone().or_else(|| Some("".to_string())).unwrap(),
239 }
240 }
241}