1use crate::common::{IpFamily, OutputFormat};
2use crate::error::AppError;
3use crate::output::write_as_ip_list_to_file;
4use ipnet::IpNet;
5use reqwest::Client;
6use serde_json::Value;
7use std::{collections::BTreeSet, str::FromStr, sync::Arc};
8use tokio::sync::Semaphore;
9use crate::constants::MAX_JSON_DOWNLOAD_BYTES;
10use crate::fetch::fetch_json_with_limit;
11use crate::common::debug_log;
12
13pub async fn get_prefixes_via_rdap(
16 client: &Client,
17 as_number: &str,
18) -> Result<(BTreeSet<IpNet>, BTreeSet<IpNet>), AppError> {
19 match fetch_ripe_stat_prefixes(client, as_number).await {
21 Ok(mut nets) => {
22 if let Ok(mut arin) = fetch_arin_originas_prefixes(client, as_number).await {
24 nets.append(&mut arin);
25 }
26 let (v4set, v6set) = dedup_and_partition(&nets);
27 return Ok((v4set, v6set));
28 }
29 Err(e) => {
30 debug_log(format!("RIPEstat fetch failed for AS{}: {}", as_number, e));
31 let nets = fetch_arin_originas_prefixes(client, as_number).await?;
33 let (v4set, v6set) = dedup_and_partition(&nets);
34 return Ok((v4set, v6set));
35 }
36 }
37}
38
39fn extract_prefixes_from_arin(v: &Value) -> Result<Vec<IpNet>, AppError> {
41 let mut nets = Vec::new();
42 if let Some(arr) = v
43 .get("arin_originas0_networkSearchResults")
44 .and_then(|v| v.as_array())
45 {
46 for obj in arr {
47 let (prefix_key, len_key) = match obj.get("ipVersion").and_then(|v| v.as_str()) {
48 Some("v4") => ("v4prefix", "length"),
49 Some("v6") => ("v6prefix", "length"),
50 _ => continue,
51 };
52 if let (Some(prefix), Some(len)) = (
53 obj.get(prefix_key).and_then(|v| v.as_str()),
54 obj.get(len_key),
55 ) {
56 let cidr = format!("{}/{}", prefix, len);
57 if let Ok(net) = IpNet::from_str(&cidr) {
58 nets.push(net);
59 }
60 }
61 }
62 }
63 Ok(nets)
64}
65
66async fn fetch_ripe_stat_prefixes(client: &Client, as_number: &str) -> Result<Vec<IpNet>, AppError> {
68 let url = format!(
70 "https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS{}",
71 as_number
72 );
73 let json: Value = fetch_json_with_limit(client, &url, MAX_JSON_DOWNLOAD_BYTES).await?;
74 let mut nets = Vec::new();
75 if let Some(prefixes) = json.get("data").and_then(|d| d.get("prefixes")).and_then(|p| p.as_array()) {
76 for obj in prefixes {
77 if let Some(pfx) = obj.get("prefix").and_then(|v| v.as_str()) {
78 if let Ok(net) = IpNet::from_str(pfx) {
79 nets.push(net);
80 }
81 }
82 }
83 }
84 Ok(nets)
85}
86
87async fn fetch_arin_originas_prefixes(client: &Client, as_number: &str) -> Result<Vec<IpNet>, AppError> {
89 let base = "https://rdap.arin.net/registry";
90 let url = format!("{base}/arin_originas0_networksbyoriginas/{as_number}");
91 let json: Value = fetch_json_with_limit(client, &url, MAX_JSON_DOWNLOAD_BYTES).await?;
92 extract_prefixes_from_arin(&json)
93}
94
95fn dedup_and_partition(nets: &[IpNet]) -> (BTreeSet<IpNet>, BTreeSet<IpNet>) {
97 let agg = IpNet::aggregate(&nets.to_vec());
98 let mut v4 = BTreeSet::new();
99 let mut v6 = BTreeSet::new();
100 for net in agg {
101 match net {
102 IpNet::V4(_) => {
103 v4.insert(net);
104 }
105 IpNet::V6(_) => {
106 v6.insert(net);
107 }
108 }
109 }
110 (v4, v6)
111}
112
113pub async fn process_as_numbers(
115 client: &Client,
116 as_numbers: &[String],
117 output_format: OutputFormat,
118 concurrency: usize,
119) -> Result<(), AppError> {
120 let max_concurrent = if concurrency == 0 { 1 } else { concurrency };
121 let semaphore = Arc::new(Semaphore::new(max_concurrent));
122
123 let handles = as_numbers
124 .iter()
125 .map(|asn| {
126 let asn_cloned = asn.clone();
127 let fmt_c = output_format;
128 let client_c = client.clone();
129 let sem_c = semaphore.clone();
130 tokio::spawn(async move {
131 let _permit = sem_c.acquire_owned().await?;
132 match get_prefixes_via_rdap(&client_c, &asn_cloned).await {
133 Ok((v4, v6)) => {
134 write_ip_list(&asn_cloned, IpFamily::V4, &v4, fmt_c).await?;
135 write_ip_list(&asn_cloned, IpFamily::V6, &v6, fmt_c).await?;
136 }
137 Err(e) => debug_log(format!("Error processing {}: {}", asn_cloned, e)),
138 };
139 Ok::<(), AppError>(())
140 })
141 })
142 .collect::<Vec<_>>();
143
144 for h in handles {
145 h.await??;
146 }
147 Ok(())
148}
149
150async fn write_ip_list(
152 as_number: &str,
153 ip_family: IpFamily,
154 ip_set: &BTreeSet<IpNet>,
155 output_format: OutputFormat,
156) -> Result<(), AppError> {
157 if ip_set.is_empty() {
158 debug_log(format!("No {} routes for {}", ip_family.as_str(), as_number));
159 } else {
160 write_as_ip_list_to_file(as_number, ip_family, ip_set, output_format).await?;
161 }
162 Ok(())
163}