har_analyzer/
analysis.rs

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}