1use regex::Regex;
2use std::env;
3use urlencoding::encode;
4
5pub struct RirUrls;
7
8impl RirUrls {
9 pub fn get_url(rir: &str, search_term: &str) -> String {
11 let encoded_term = encode(search_term);
12
13 match rir.to_uppercase().as_str() {
14 "RIPE" => format!("https://apps.db.ripe.net/db-web-ui/query?searchtext={}", encoded_term),
15 "ARIN" => format!("https://search.arin.net/rdap/?query={}", encoded_term),
16 "APNIC" => format!("https://wq.apnic.net/apnic-bin/whois.pl?searchtext={}", encoded_term),
17 "LACNIC" => {
18 format!("https://query.milacnic.lacnic.net/home?searchtext={}", encoded_term)
20 },
21 "AFRINIC" => format!("https://afrinic.net/whois?searchtext={}", encoded_term),
22 _ => {
23 format!("https://apps.db.ripe.net/db-web-ui/query?searchtext={}", encoded_term)
25 }
26 }
27 }
28}
29
30pub fn detect_rir_from_source(response: &str) -> Vec<&'static str> {
32 let mut rirs = Vec::new();
33
34 let source_regex = Regex::new(r"(?m)^source:\s*([A-Z-]+)").unwrap();
36
37 for caps in source_regex.captures_iter(response) {
38 if let Some(source) = caps.get(1) {
39 let source_value = source.as_str().trim();
40 let rir = match source_value {
41 "RIPE" => Some("ripe"),
42 "ARIN" => Some("arin"),
43 "APNIC" => Some("apnic"),
44 "LACNIC" => Some("lacnic"),
45 "AFRINIC" => Some("afrinic"),
46 _ => None,
47 };
48
49 if let Some(rir) = rir {
50 if !rirs.contains(&rir) {
51 rirs.push(rir);
52 }
53 }
54 }
55 }
56
57 rirs
58}
59
60pub fn detect_rir(response: &str) -> Option<&'static str> {
62 let rirs = detect_rir_from_source(response);
64 if !rirs.is_empty() {
65 return Some(rirs[0]);
66 }
67
68 if response.contains("% This is the RIPE Database query service") ||
70 response.contains("whois.ripe.net") ||
71 response.contains("RIPE-NCC") {
72 return Some("ripe");
73 }
74
75 if response.contains("American Registry for Internet Numbers") ||
76 response.contains("ARIN WHOIS data") ||
77 response.contains("NetRange:") ||
78 response.contains("whois.arin.net") {
79 return Some("arin");
80 }
81
82 if response.contains("Asia Pacific Network Information Centre") ||
83 response.contains("APNIC WHOIS Database") ||
84 response.contains("whois.apnic.net") {
85 return Some("apnic");
86 }
87
88 if response.contains("Latin American and Caribbean IP address Regional Registry") ||
89 response.contains("LACNIC WHOIS") ||
90 response.contains("whois.lacnic.net") {
91 return Some("lacnic");
92 }
93
94 if response.contains("African Network Information Centre") ||
95 response.contains("AFRINIC WHOIS") ||
96 response.contains("whois.afrinic.net") {
97 return Some("afrinic");
98 }
99
100 None
101}
102
103pub fn is_rir_response(response: &str) -> bool {
105 !detect_rir_from_source(response).is_empty() || detect_rir(response).is_some()
106}
107
108pub fn is_ripe_response(response: &str) -> bool {
110 detect_rir_from_source(response).contains(&"ripe") || detect_rir(response) == Some("ripe")
111}
112
113pub fn terminal_supports_hyperlinks() -> bool {
115 if env::var("WT_SESSION").is_ok() || env::var("WT_PROFILE_ID").is_ok() {
117 return true;
118 }
119
120 if env::var("TERM_PROGRAM").map_or(false, |term| term == "vscode") {
122 return true;
123 }
124
125 if let Ok(term) = env::var("TERM") {
127 if term.contains("xterm") ||
129 term.contains("screen") ||
130 term.contains("tmux") ||
131 term == "alacritty" ||
132 term == "kitty" ||
133 term == "foot" ||
134 term.contains("256color") {
135 return true;
136 }
137 }
138
139 if env::var("VTE_VERSION").is_ok() {
141 return true;
142 }
143
144 if env::var("ITERM_SESSION_ID").is_ok() || env::var("TERM_PROGRAM").map_or(false, |term| term == "iTerm.app") {
146 return true;
147 }
148
149 if env::var("WEZTERM_EXECUTABLE").is_ok() || env::var("TERM_PROGRAM").map_or(false, |term| term == "WezTerm") {
151 return true;
152 }
153
154 if env::var("TERM_PROGRAM").map_or(false, |term| term == "Hyper") {
156 return true;
157 }
158
159 if cfg!(windows) {
161 if env::var("SESSIONNAME").is_ok() ||
163 env::var("COMPUTERNAME").is_ok() {
164 if let Ok(term_program) = env::var("TERM_PROGRAM") {
166 if term_program.contains("WindowsTerminal") || term_program.contains("wt") {
167 return true;
168 }
169 }
170 }
171 }
172
173 true
175}
176
177pub fn create_hyperlink(url: &str, text: &str) -> String {
179 if !terminal_supports_hyperlinks() {
180 return text.to_string();
181 }
182
183 format!("\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\", url, text)
184}
185
186fn split_response_by_source(response: &str) -> Vec<(String, &'static str)> {
188 let mut blocks = Vec::new();
189 let lines: Vec<&str> = response.lines().collect();
190 let mut current_block = String::new();
191 let mut current_rir = None;
192
193 for line in lines {
194 if let Some(caps) = Regex::new(r"^source:\s*([A-Z-]+)").unwrap().captures(line) {
196 if let Some(source) = caps.get(1) {
197 let source_value = source.as_str().trim();
198 let rir = match source_value {
199 "RIPE" => Some("ripe"),
200 "ARIN" => Some("arin"),
201 "APNIC" => Some("apnic"),
202 "LACNIC" => Some("lacnic"),
203 "AFRINIC" => Some("afrinic"),
204 _ => None,
205 };
206
207 if let Some(current) = current_rir {
209 if rir != Some(current) && !current_block.trim().is_empty() {
210 blocks.push((current_block.clone(), current));
211 current_block.clear();
212 }
213 }
214
215 current_rir = rir;
216 }
217 }
218
219 current_block.push_str(line);
220 current_block.push('\n');
221 }
222
223 if let Some(rir) = current_rir {
225 if !current_block.trim().is_empty() {
226 blocks.push((current_block, rir));
227 }
228 } else if !current_block.trim().is_empty() {
229 if let Some(rir) = detect_rir(¤t_block) {
231 blocks.push((current_block, rir));
232 }
233 }
234
235 if blocks.is_empty() {
237 if let Some(rir) = detect_rir(response) {
238 blocks.push((response.to_string(), rir));
239 }
240 }
241
242 blocks
243}
244
245pub struct RirHyperlinkProcessor;
247
248impl RirHyperlinkProcessor {
249 pub fn new() -> Self {
250 Self
251 }
252
253 pub fn process(&self, response: &str) -> String {
255 if !terminal_supports_hyperlinks() {
256 return response.to_string();
257 }
258
259 let blocks = split_response_by_source(response);
261
262 if blocks.is_empty() {
263 return response.to_string();
264 }
265
266 let mut processed_blocks = Vec::new();
267
268 for (block, rir) in blocks {
269 let mut processed_block = block;
270
271 match rir {
273 "ripe" => self.process_ripe(&mut processed_block),
274 "arin" => self.process_arin(&mut processed_block),
275 "apnic" => self.process_apnic(&mut processed_block),
276 "lacnic" => self.process_lacnic(&mut processed_block),
277 "afrinic" => self.process_afrinic(&mut processed_block),
278 _ => {}
279 }
280
281 processed_blocks.push(processed_block);
282 }
283
284 processed_blocks.join("")
285 }
286
287 fn apply_patterns(&self, processed: &mut String, patterns: Vec<(&str, &str)>, rir: &str) {
288 for (pattern_str, _) in patterns {
289 if let Ok(pattern) = Regex::new(pattern_str) {
290 *processed = pattern.replace_all(processed, |caps: ®ex::Captures| {
291 let _full_match = caps.get(0).unwrap().as_str();
292 let prefix = caps.get(1).unwrap().as_str();
293 let value = caps.get(2).unwrap().as_str();
294
295 let url = RirUrls::get_url(rir, value);
297 let hyperlinked_value = create_hyperlink(&url, value);
298
299 format!("{}{}", prefix, hyperlinked_value)
300 }).to_string();
301 }
302 }
303 }
304
305 fn process_ripe(&self, processed: &mut String) {
306 let patterns = vec![
307 (r"(?m)^(aut-num:\s+)(AS\d+)", ""),
309 (r"(?m)^(origin:\s+)(AS\d+)", ""),
310
311 (r"(?m)^(inetnum:\s+)([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\s*-\s*[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", ""),
313 (r"(?m)^(inet6num:\s+)([0-9a-fA-F:]+/\d+)", ""),
314 (r"(?m)^(route:\s+)([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/\d+)", ""),
315 (r"(?m)^(route6:\s+)([0-9a-fA-F:]+/\d+)", ""),
316
317 (r"(?m)^(organisation:\s+)(ORG-[A-Z0-9-]+)", ""),
319 (r"(?m)^(org:\s+)(ORG-[A-Z0-9-]+)", ""),
320
321 (r"(?m)^(nic-hdl:\s+)([A-Z0-9-]+)", ""),
323 (r"(?m)^(admin-c:\s+)([A-Z0-9-]+)", ""),
324 (r"(?m)^(tech-c:\s+)([A-Z0-9-]+)", ""),
325
326 (r"(?m)^(mntner:\s+)([A-Z][A-Z0-9-]*)", ""),
328 (r"(?m)^(mnt-by:\s+)([A-Z][A-Z0-9-]*)", ""),
329
330 (r"(?m)^(domain:\s+)([a-zA-Z0-9.-]+\.arpa)", ""),
332
333 (r"(?m)^(as-block:\s+)(AS\d+\s*-\s*AS\d+)", ""),
335 ];
336
337 self.apply_patterns(processed, patterns, "RIPE");
338 }
339
340 fn process_arin(&self, processed: &mut String) {
341 let patterns = vec![
342 (r"(?m)^(NetRange:\s+)([0-9.-]+)", ""),
344 (r"(?m)^(CIDR:\s+)([0-9./]+)", ""),
345 (r"(?m)^(OriginAS:\s+)(AS\d+)", ""),
346 (r"(?m)^(OrgId:\s+)([A-Z0-9-]+)", ""),
347 (r"(?m)^(NetName:\s+)([A-Z0-9-]+)", ""),
348
349 (r"(?m)^(aut-num:\s+)(AS\d+)", ""),
351 (r"(?m)^(origin:\s+)(AS\d+)", ""),
352 (r"(?m)^(inetnum:\s+)([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\s*-\s*[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", ""),
353 (r"(?m)^(inet6num:\s+)([0-9a-fA-F:]+/\d+)", ""),
354 ];
355
356 self.apply_patterns(processed, patterns, "ARIN");
357 }
358
359 fn process_apnic(&self, processed: &mut String) {
360 let patterns = vec![
361 (r"(?m)^(aut-num:\s+)(AS\d+)", ""),
363 (r"(?m)^(origin:\s+)(AS\d+)", ""),
364 (r"(?m)^(inetnum:\s+)([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\s*-\s*[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", ""),
365 (r"(?m)^(inet6num:\s+)([0-9a-fA-F:]+/\d+)", ""),
366 (r"(?m)^(nic-hdl:\s+)([A-Z0-9-]+)", ""),
367 (r"(?m)^(admin-c:\s+)([A-Z0-9-]+)", ""),
368 (r"(?m)^(tech-c:\s+)([A-Z0-9-]+)", ""),
369 ];
370
371 self.apply_patterns(processed, patterns, "APNIC");
372 }
373
374 fn process_lacnic(&self, processed: &mut String) {
375 let patterns = vec![
376 (r"(?m)^(aut-num:\s+)(AS\d+)", ""),
378 (r"(?m)^(origin:\s+)(AS\d+)", ""),
379 (r"(?m)^(inetnum:\s+)([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\s*-\s*[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", ""),
380 (r"(?m)^(inet6num:\s+)([0-9a-fA-F:]+/\d+)", ""),
381 (r"(?m)^(nic-hdl:\s+)([A-Z0-9-]+)", ""),
382 ];
383
384 self.apply_patterns(processed, patterns, "LACNIC");
385 }
386
387 fn process_afrinic(&self, processed: &mut String) {
388 let patterns = vec![
389 (r"(?m)^(aut-num:\s+)(AS\d+)", ""),
391 (r"(?m)^(origin:\s+)(AS\d+)", ""),
392 (r"(?m)^(inetnum:\s+)([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\s*-\s*[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", ""),
393 (r"(?m)^(inet6num:\s+)([0-9a-fA-F:]+/\d+)", ""),
394 (r"(?m)^(nic-hdl:\s+)([A-Z0-9-]+)", ""),
395 ];
396
397 self.apply_patterns(processed, patterns, "AFRINIC");
398 }
399}
400
401impl Default for RirHyperlinkProcessor {
402 fn default() -> Self {
403 Self::new()
404 }
405}
406
407pub type RipeHyperlinkProcessor = RirHyperlinkProcessor;
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413
414 #[test]
415 fn test_detect_rir_from_source() {
416 let multi_rir_response = r#"
417as-block: AS137530 - AS138553
418descr: APNIC ASN block
419source: APNIC
420
421aut-num: AS3333
422as-name: RIPE-NCC-AS
423source: RIPE
424 "#;
425
426 let rirs = detect_rir_from_source(multi_rir_response);
427 assert!(rirs.contains(&"apnic"));
428 assert!(rirs.contains(&"ripe"));
429 }
430
431 #[test]
432 fn test_split_response_by_source() {
433 let multi_rir_response = r#"
434as-block: AS137530 - AS138553
435source: APNIC
436
437aut-num: AS3333
438source: RIPE
439 "#;
440
441 let blocks = split_response_by_source(multi_rir_response);
442 assert_eq!(blocks.len(), 2);
443 }
444
445 #[test]
446 fn test_create_hyperlink() {
447 let url = "https://example.com";
448 let text = "Example";
449
450 let result = create_hyperlink(url, text);
451 assert!(result.contains("Example"));
452 }
453
454 #[test]
455 fn test_rir_urls() {
456 let query_url = RirUrls::get_url("RIPE", "AS3333");
457 assert!(query_url.contains("AS3333"));
458 assert!(!query_url.contains("types=")); assert!(query_url.contains("apps.db.ripe.net"));
460
461 let arin_url = RirUrls::get_url("ARIN", "AS3333");
463 assert!(arin_url.contains("search.arin.net"));
464 assert!(arin_url.contains("AS3333"));
465
466 let apnic_url = RirUrls::get_url("APNIC", "AS3333");
467 assert!(apnic_url.contains("wq.apnic.net"));
468 assert!(apnic_url.contains("AS3333"));
469
470 let lacnic_url = RirUrls::get_url("LACNIC", "AS3333");
471 assert!(lacnic_url.contains("query.milacnic.lacnic.net"));
472 assert!(lacnic_url.contains("AS3333"));
473
474 let afrinic_url = RirUrls::get_url("AFRINIC", "AS3333");
475 assert!(afrinic_url.contains("afrinic.net"));
476 assert!(afrinic_url.contains("AS3333"));
477 }
478}