diff --git a/querying/src/asn.rs b/querying/src/asn.rs index 8b6ca4c..d6f443f 100644 --- a/querying/src/asn.rs +++ b/querying/src/asn.rs @@ -129,6 +129,12 @@ pub struct AsnCache { cache: Arc>>, } +impl Default for AsnCache { + fn default() -> Self { + Self::new() + } +} + impl AsnCache { pub fn new() -> Self { Self { diff --git a/querying/src/cache.rs b/querying/src/cache.rs index e321f29..1446e3a 100644 --- a/querying/src/cache.rs +++ b/querying/src/cache.rs @@ -142,8 +142,7 @@ impl DatabaseCache { }, ], }; - let metadata = serde_json::to_vec_pretty(&metadata) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let metadata = serde_json::to_vec_pretty(&metadata).map_err(io::Error::other)?; self.write_file(METADATA_FILE, &metadata) } diff --git a/querying/src/geoip.rs b/querying/src/geoip.rs index 8e5d125..c0d58e1 100644 --- a/querying/src/geoip.rs +++ b/querying/src/geoip.rs @@ -3,7 +3,6 @@ use async_trait::async_trait; use maxminddb::geoip2::{City, Country}; use maxminddb::{MaxMindDbError, geoip2}; use serde::Serialize; -use std::io; use std::io::Error; use std::net::IpAddr; @@ -34,6 +33,12 @@ impl Default for IpInfo { } } +impl Default for GeoIp { + fn default() -> Self { + Self::new() + } +} + impl GeoIp { pub fn new() -> Self { GeoIp { @@ -80,11 +85,10 @@ impl GeoIp { let country_code = country .as_ref() - .map(|c| c.country.iso_code) - .flatten() + .and_then(|c| c.country.iso_code) .map(|c| c.to_string()); - let city_geo_name_id = city.as_ref().map(|c| c.city.geoname_id).flatten(); + let city_geo_name_id = city.as_ref().and_then(|c| c.city.geoname_id); let mut location = (None, None); if let Some(city) = city { @@ -141,7 +145,6 @@ impl Updatable for GeoIp { } async fn install(&mut self, (asn, country, city): Self::Base) -> Result<(), Error> { - self.update(asn, country, city) - .map_err(|e| Error::new(io::ErrorKind::Other, e)) + self.update(asn, country, city).map_err(Error::other) } } diff --git a/querying/src/lib.rs b/querying/src/lib.rs index 7c60a99..e7f78d5 100644 --- a/querying/src/lib.rs +++ b/querying/src/lib.rs @@ -100,7 +100,7 @@ impl Checker { } }; - let reverse_lookup = if let Some(ip) = ips.get(0).cloned() { + let reverse_lookup = if let Some(ip) = ips.first().copied() { match self.resolver.lookup_ptr(ip).await { Ok(ptr) => ptr, Err(e) => { @@ -113,7 +113,7 @@ impl Checker { }; let geo_ip = self.geo_ip.load(); - let geo = match ips.get(0).map(|ip| geo_ip.lookup(ip.clone())) { + let geo = match ips.first().map(|ip| geo_ip.lookup(*ip)) { None => IpInfo::default(), Some(Ok(ip)) => ip, Some(Err(e)) => { @@ -172,12 +172,11 @@ impl Checker { .collect(); for prefix in &prefixes { - if let Ok(ipnet) = prefix.parse::() { - if cdn_list.contains(&ipnet.network()).is_some() { - if !blocked_prefixes.contains(prefix) { - blocked_prefixes.push(prefix.clone()); - } - } + if let Ok(ipnet) = prefix.parse::() + && cdn_list.contains(&ipnet.network()).is_some() + && !blocked_prefixes.contains(prefix) + { + blocked_prefixes.push(prefix.clone()); } } @@ -215,7 +214,7 @@ impl Checker { } pub fn last_update(&self) -> Option> { - self.rx.borrow().clone() + *self.rx.borrow() } pub async fn download_all() -> Result { diff --git a/querying/src/lists.rs b/querying/src/lists.rs index 4a0871a..a925f62 100644 --- a/querying/src/lists.rs +++ b/querying/src/lists.rs @@ -14,6 +14,12 @@ pub struct CdnList { trie: IpnetTrie, } +impl Default for CdnList { + fn default() -> Self { + Self::new() + } +} + #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Eq, PartialEq, Hash)] pub struct NetworkRecord { pub provider: String, @@ -97,6 +103,12 @@ pub struct RuBlacklist { pub domain_count: usize, } +impl Default for RuBlacklist { + fn default() -> Self { + Self::new() + } +} + impl RuBlacklist { pub fn new() -> RuBlacklist { RuBlacklist { @@ -176,6 +188,35 @@ impl RuBlacklist { } } +#[async_trait] +impl Updatable for RuBlacklist { + type Base = (VecDeque, VecDeque, VecDeque); + + async fn download() -> Result { + Ok(( + VecDeque::from( + fetch_db(Self::get_url( + "RKN_NETS", + "https://antifilter.network/download/ipsum.lst", + )) + .await?, + ), + VecDeque::from( + fetch_db(Self::get_url( + "RKN_DOMAINS", + "https://antifilter.download/list/domains.lst", + )) + .await?, + ), + VecDeque::from(include_bytes!("../dist-domains.txt").to_vec()), + )) + } + + async fn install(&mut self, (nets, domains, custom_domains): Self::Base) -> Result<(), Error> { + self.update(nets, domains, custom_domains) + } +} + #[cfg(test)] mod tests { use super::RuBlacklist; @@ -205,32 +246,3 @@ mod tests { assert_eq!(list.contains_domain("notblocked.example"), None); } } - -#[async_trait] -impl Updatable for RuBlacklist { - type Base = (VecDeque, VecDeque, VecDeque); - - async fn download() -> Result { - Ok(( - VecDeque::from( - fetch_db(Self::get_url( - "RKN_NETS", - "https://antifilter.network/download/ipsum.lst", - )) - .await?, - ), - VecDeque::from( - fetch_db(Self::get_url( - "RKN_DOMAINS", - "https://antifilter.download/list/domains.lst", - )) - .await?, - ), - VecDeque::from(include_bytes!("../dist-domains.txt").to_vec()), - )) - } - - async fn install(&mut self, (nets, domains, custom_domains): Self::Base) -> Result<(), Error> { - self.update(nets, domains, custom_domains) - } -} diff --git a/querying/src/resolver.rs b/querying/src/resolver.rs index 79ac0ce..9887b99 100644 --- a/querying/src/resolver.rs +++ b/querying/src/resolver.rs @@ -3,7 +3,7 @@ use hickory_resolver::net::runtime::TokioRuntimeProvider; use hickory_resolver::net::{DnsError, NetError}; use hickory_resolver::proto::ProtoError; use hickory_resolver::proto::rr::RData; -use std::io::{Error, ErrorKind}; +use std::io::Error; use std::net::IpAddr; use std::sync::Arc; use thiserror::Error; @@ -93,6 +93,6 @@ fn map_resolve_error(error: NetError) -> ResolveError { { ResolveError::NxDomain } - _ => ResolveError::Other(Error::new(ErrorKind::Other, error)), + _ => ResolveError::Other(Error::other(error)), } } diff --git a/querying/src/target.rs b/querying/src/target.rs index a869d71..02d1b5c 100644 --- a/querying/src/target.rs +++ b/querying/src/target.rs @@ -17,26 +17,27 @@ pub enum Target { impl From<&str> for Target { fn from(input: &str) -> Self { - if input.to_lowercase().starts_with("as") { - if let Ok(asn) = input[2..].parse::() { - if asn >= 1 { - return Target::Asn(asn); - } - } + if input + .get(..2) + .is_some_and(|prefix| prefix.eq_ignore_ascii_case("as")) + && let Ok(asn) = input[2..].parse::() + && asn >= 1 + { + return Target::Asn(asn); } - if input.contains('/') { - if let Ok(ipv4_net) = input.parse::() { - if ipv4_net.prefix_len() >= 8 { - return Target::Ipv4Subnet(ipv4_net); - } - } + if input.contains('/') + && let Ok(ipv4_net) = input.parse::() + && ipv4_net.prefix_len() >= 8 + { + return Target::Ipv4Subnet(ipv4_net); + } - if let Ok(ipv6_net) = input.parse::() { - if ipv6_net.prefix_len() >= 32 { - return Target::Ipv6Subnet(ipv6_net); - } - } + if input.contains('/') + && let Ok(ipv6_net) = input.parse::() + && ipv6_net.prefix_len() >= 32 + { + return Target::Ipv6Subnet(ipv6_net); } if let Ok(ipv4) = input.parse::() { @@ -47,10 +48,10 @@ impl From<&str> for Target { return Target::Ipv6(ipv6); } - if let Ok(url) = input.parse::() { - if let Some(host) = url.host_str() { - return Target::Domain(host.trim_end_matches('.').to_string()); - } + if let Ok(url) = input.parse::() + && let Some(host) = url.host_str() + { + return Target::Domain(host.trim_end_matches('.').to_string()); } Target::Domain(input.trim_end_matches('.').to_string()) } diff --git a/querying/src/updater.rs b/querying/src/updater.rs index 98a2f99..4ac27f7 100644 --- a/querying/src/updater.rs +++ b/querying/src/updater.rs @@ -4,30 +4,28 @@ use indicatif::{ProgressBar, ProgressStyle}; use log::info; use reqwest::IntoUrl; use std::fmt::Display; -use std::io; use std::io::Error; pub async fn fetch_db(url: T) -> Result, Error> { info!("Fetching {}", url); let response = reqwest::get(url) .await - .map_err(|e| Error::new(io::ErrorKind::Other, e))? + .map_err(Error::other)? .error_for_status() - .map_err(|e| Error::new(io::ErrorKind::Other, e))?; + .map_err(Error::other)?; let total_size = response.content_length().unwrap_or(0); let pb = ProgressBar::new(total_size); pb.set_style(ProgressStyle::default_bar() .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})") - .map_err(|e| Error::new(io::ErrorKind::Other, e))? + .map_err(Error::other)? .progress_chars("#>-")); - let mut bytes = Vec::new(); - bytes.reserve(total_size as usize); + let mut bytes = Vec::with_capacity(total_size as usize); let mut stream = response.bytes_stream(); while let Some(chunk_result) = stream.next().await { - let chunk = chunk_result.map_err(|e| Error::new(io::ErrorKind::Other, e))?; + let chunk = chunk_result.map_err(Error::other)?; bytes.extend(&chunk); pb.inc(chunk.len() as u64); } diff --git a/reporter/src/counter.rs b/reporter/src/counter.rs index 7f925ce..341b54b 100644 --- a/reporter/src/counter.rs +++ b/reporter/src/counter.rs @@ -17,9 +17,9 @@ pub struct Counter { impl Counter { pub fn save_results(&self, output: &PathBuf) -> anyhow::Result<()> { let mut out = csv::WriterBuilder::new().from_path(output)?; - out.write_record(&["target", "evidence"])?; + out.write_record(["target", "evidence"])?; for (target, evidence) in &self.results { - out.write_record(&[target, &evidence.to_string()])?; + out.write_record([target, &evidence.to_string()])?; } info!("Saved results to {:?}", output); Ok(()) @@ -59,16 +59,51 @@ impl Counter { impl Display for Counter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let total = self.total(); + let percentage = |count| { + if total == 0 { + 0.0 + } else { + count as f32 / total as f32 * 100.0 + } + }; write!( f, "OK {} ({:.2}%) | Blocked {} (early: {}) ({:.2}%) | Error {} ({:.2}%)", self.ok, - self.ok as f32 / total as f32 * 100.0, + percentage(self.ok), self.block, self.early, - self.block as f32 / total as f32 * 100.0, + percentage(self.block), self.err, - self.err as f32 / total as f32 * 100.0 + percentage(self.err) ) } } + +#[cfg(test)] +mod tests { + use super::Counter; + use reports::Evidence; + + #[test] + fn empty_counter_uses_zero_percentages() { + assert_eq!( + Counter::default().to_string(), + "OK 0 (0.00%) | Blocked 0 (early: 0) (0.00%) | Error 0 (0.00%)" + ); + } + + #[test] + fn counter_tracks_all_evidence_classes() { + let mut counter = Counter::default(); + counter.add("ok.example", Evidence::Ok); + counter.add("blocked.example", Evidence::Blocked); + counter.add("error.example", Evidence::ConnectError); + + assert_eq!(counter.total(), 3); + assert_eq!( + counter.to_string(), + "OK 1 (33.33%) | Blocked 1 (early: 0) (33.33%) | Error 1 (33.33%)" + ); + } +} diff --git a/reporter/src/main.rs b/reporter/src/main.rs index 1812c24..bc8c947 100644 --- a/reporter/src/main.rs +++ b/reporter/src/main.rs @@ -32,6 +32,14 @@ enum Verbosity { All, } +fn parse_positive_usize(value: &str) -> Result { + match value.parse::() { + Ok(value) if value > 0 => Ok(value), + Ok(_) => Err("значение должно быть больше нуля".to_string()), + Err(error) => Err(format!("некорректное целое число: {error}")), + } +} + #[derive(Parser, Debug, Clone)] #[command( author, @@ -57,7 +65,7 @@ struct Args { timeout_secs: u64, /// Maximum concurrent probes. Make sure that it doesn't exceed 'ulimit -n' - #[arg(short, long = "probes", default_value_t = 1000)] + #[arg(short, long = "probes", default_value_t = 1000, value_parser = parse_positive_usize)] probe_count: usize, /// Display probing results in console @@ -65,7 +73,7 @@ struct Args { verbosity: Verbosity, /// Attempts to establish connection - #[arg(short, long, default_value_t = 2)] + #[arg(short, long, default_value_t = 2, value_parser = parse_positive_usize)] retry_count: usize, /// Try using plain HTTP without TLS @@ -102,7 +110,7 @@ impl Args { ReporterConfig { http: self.http, tx_junk: self.tx, - ip: self.ip.clone(), + ip: self.ip, path: self.path.clone(), retry_count: self.retry_count, timeout_secs: self.timeout_secs, @@ -117,10 +125,14 @@ fn build_client(args: &Args, attempt: usize) -> reqwest::Result { .redirect(Policy::none()) .use_rustls_tls() .dns_resolver(Arc::new(Resolver::new(args.ip))) - .read_timeout(Duration::from_secs(args.timeout_secs * attempt as u64)) + .read_timeout(retry_read_timeout(args.timeout_secs, attempt)) .timeout(Duration::from_secs(15)); - Ok(client.build()?) + client.build() +} + +fn retry_read_timeout(timeout_secs: u64, attempt: usize) -> Duration { + Duration::from_secs(timeout_secs.saturating_mul(attempt as u64)) } #[tokio::main] @@ -145,7 +157,7 @@ async fn main() -> Result<()> { let targets: Vec = targets .lines() .take(args.count) - .map(|s| s.split(",").last().unwrap().to_string()) + .map(|s| s.split(',').next_back().unwrap().to_string()) .collect(); info!( @@ -244,9 +256,9 @@ async fn upload_results( let uploaded = uploaded.send().await?; if uploaded.status().is_success() { - info!("Uploaded ({})!", uploaded.status().to_string()); + info!("Uploaded ({})!", uploaded.status()); } else { - warn!("Upload failed: {}", uploaded.status().to_string()); + warn!("Upload failed: {}", uploaded.status()); } info!("Agency response: {}", uploaded.text().await?); Ok(()) @@ -288,7 +300,7 @@ async fn check_target(args: &Args, target: &str) -> Result Result Resolver { Resolver { - ip: SocketAddr::from(SocketAddr::new(ip, 0)), + ip: SocketAddr::new(ip, 0), } } } impl Resolve for Resolver { fn resolve(&self, _: Name) -> Resolving { - let ip = self.ip.clone(); + let ip = self.ip; Box::pin(async move { Ok(Addrs::from(Box::new(vec![ip].into_iter()))) }) } } diff --git a/website/src/agency.rs b/website/src/agency.rs index a4d83ea..23d8ca2 100644 --- a/website/src/agency.rs +++ b/website/src/agency.rs @@ -10,6 +10,10 @@ use sqlx::postgres::PgPool; pub struct Agency { pub id: i32, + #[expect( + dead_code, + reason = "поле входит в SQLx offline-запрос; его удаление требует обновления метаданных на эталонной PostgreSQL" + )] pub name: String, } diff --git a/website/src/api/check.rs b/website/src/api/check.rs index 505cafe..4cb4502 100644 --- a/website/src/api/check.rs +++ b/website/src/api/check.rs @@ -58,7 +58,7 @@ pub async fn check( .map_err(|_| Status::InternalServerError)?; let id: Option = if let Ok(check) = &check { - match save_query(&mut *db, &target, check, addr, checker.read().await).await { + match save_query(&mut db, &target, check, addr, checker.read().await).await { Ok(id) => Some(id.to_string()), Err(e) => { warn!("api: failed to save check: {:?}", e); @@ -70,7 +70,7 @@ pub async fn check( }; let whitelist: Option = if let Target::Domain(domain) = &target { - check_whitelist(domain, &mut *db) + check_whitelist(domain, &mut db) .await .map_err(|_| Status::InternalServerError)? } else { diff --git a/website/src/api/probe.rs b/website/src/api/probe.rs index e05f334..fc97319 100644 --- a/website/src/api/probe.rs +++ b/website/src/api/probe.rs @@ -544,7 +544,7 @@ mod tests { config.hosts.push(Host { id: "test".to_string(), host: "192.0.2.1".to_string(), - host_type: HostType::Blacklist, + host_type: HostType::Whitelist, file_path: String::new(), timeout_sec: 1, min_data: 1, @@ -567,6 +567,28 @@ mod tests { ); } + #[test] + fn reports_whitelist_when_blacklist_host_returns_data() { + let mut config = empty_config(); + config.hosts.push(Host { + id: "test".to_string(), + host: "192.0.2.1".to_string(), + host_type: HostType::Blacklist, + file_path: String::new(), + timeout_sec: 1, + min_data: 1, + }); + let results = vec![HostProbeResult { + host_id: "test".to_string(), + probe_evidence: ProbeEvidence::ClientHello, + }]; + + assert_eq!( + build_probe_verdicts(&results, &config, None, None, None, false), + vec!["whitelist"] + ); + } + fn empty_config() -> ProbeConfig { ProbeConfig { version: String::new(), diff --git a/website/src/db.rs b/website/src/db.rs index 91d9599..a72df02 100644 --- a/website/src/db.rs +++ b/website/src/db.rs @@ -98,7 +98,7 @@ impl<'r> FromRequest<'r> for Agency { let mut db = try_outcome!( pool.acquire() .await - .map_err(|e| Some(e)) + .map_err(Some) .or_forward(Status::InternalServerError) ); let token = request.headers().get_one("Authorization"); @@ -114,7 +114,7 @@ impl<'r> FromRequest<'r> for Agency { sqlx::query!("SELECT id, name FROM reporters WHERE token = $1", token) .fetch_optional(&mut *db) .await - .map_err(|e| Some(e)) + .map_err(Some) .or_forward(Status::InternalServerError) ); agency @@ -152,7 +152,6 @@ pub async fn check_whitelist( ) .fetch_optional(db) .await - .into() } #[derive(Debug, Serialize, sqlx::FromRow)] @@ -190,5 +189,4 @@ ORDER BY b.bin;", ) .fetch_all(db) .await - .into() } diff --git a/website/src/mqtt.rs b/website/src/mqtt.rs index b18e130..843043b 100644 --- a/website/src/mqtt.rs +++ b/website/src/mqtt.rs @@ -324,7 +324,7 @@ struct ParsedProbeConfig { } fn parse_probe_hosts(contents: &str) -> Result { - let config: ProbeHostsFile = toml::from_str(&contents).map_err(PublishError::ConfigParse)?; + let config: ProbeHostsFile = toml::from_str(contents).map_err(PublishError::ConfigParse)?; let hosts = config .hosts diff --git a/website/src/whitelist.rs b/website/src/whitelist.rs index 1f9a211..c8469f9 100644 --- a/website/src/whitelist.rs +++ b/website/src/whitelist.rs @@ -39,20 +39,14 @@ pub async fn export_csv( } }; - let mut db = pool - .acquire() - .await - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let mut db = pool.acquire().await.map_err(io::Error::other)?; - let mut stream = db - .copy_out_raw(query) - .await - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let mut stream = db.copy_out_raw(query).await.map_err(io::Error::other)?; let mut data = Vec::default(); while let Some(bytes_result) = stream.next().await { - let bytes = bytes_result.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let bytes = bytes_result.map_err(io::Error::other)?; data.extend(bytes) } @@ -75,7 +69,7 @@ pub async fn histogram( .await .map_err(|_| Status::InternalServerError)?; Ok(Json( - collect_histogram(&mut *db, 50, limit, filter.is_some()) + collect_histogram(&mut db, 50, limit, filter.is_some()) .await .map_err(|_| Status::InternalServerError)?, ))