gooty_proxy/inspection/ownership.rs
1//! # Ownership Module
2//!
3//! This module provides functionality for determining ownership information
4//! for IP addresses and proxy servers, including organization details and
5//! Autonomous System Numbers (ASNs).
6//!
7//! ## Overview
8//!
9//! The module contains types and functions for:
10//!
11//! - Looking up IP address ownership through various data sources
12//! - Retrieving Autonomous System Numbers (ASNs) and network information
13//! - Obtaining organization details associated with IP addresses
14//! - Accessing network-level metadata about IP ranges
15//!
16//! This information helps classify proxies by their operators, detect
17//! datacenter vs residential proxies, and identify potentially malicious
18//! sources.
19//!
20//! ## Examples
21//!
22//! ```
23//! use gooty_proxy::inspection::{OwnershipLookup, Organization};
24//! use std::net::IpAddr;
25//!
26//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
27//! // Create a new ownership lookup service
28//! let lookup = OwnershipLookup::new();
29//!
30//! // Look up an IP address
31//! let ip: IpAddr = "8.8.8.8".parse()?;
32//! let network_info = lookup.lookup(ip).await?;
33//!
34//! // Access organization information
35//! if let Some(org) = &network_info.organization {
36//! println!("Organization: {}", org.name.as_deref().unwrap_or("Unknown"));
37//! println!("ASN: {}", org.asn.as_deref().unwrap_or("Unknown"));
38//! }
39//! # Ok(())
40//! # }
41//! ```
42
43use crate::definitions::errors::{OwnershipError, OwnershipResult};
44use crate::inspection::Location;
45use reqwest::Client;
46use serde::{Deserialize, Serialize};
47use std::net::IpAddr;
48use std::time::Duration;
49
50/// Represents the ownership information of an organization.
51///
52/// This structure contains details about organizations that own or operate
53/// IP address blocks, including their name, ASN, and parent organization
54/// if available.
55///
56/// # Examples
57///
58/// ```
59/// use gooty_proxy::inspection::Organization;
60///
61/// let org = Organization::new(
62/// Some("Google LLC".to_string()),
63/// Some("15169".to_string())
64/// );
65///
66/// assert_eq!(org.name.as_deref(), Some("Google LLC"));
67/// assert_eq!(org.asn.as_deref(), Some("15169"));
68/// ```
69#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
70pub struct Organization {
71 /// The name of the organization
72 pub name: Option<String>,
73
74 /// The ASN (Autonomous System Number) of the organization
75 pub asn: Option<String>,
76
77 /// The parent organization if available
78 pub parent: Option<Box<Organization>>,
79}
80
81/// Network information associated with an IP address
82///
83/// Contains details about the network an IP address belongs to,
84/// including CIDR notation, organization ownership, and geographic location.
85///
86/// # Examples
87///
88/// ```
89/// use gooty_proxy::inspection::{NetworkInfo, Organization, Location};
90///
91/// // Create network info with CIDR and organization
92/// let org = Organization::new(Some("Example ISP".to_string()), Some("12345".to_string()));
93/// let network = NetworkInfo {
94/// cidr: Some("192.168.0.0/24".to_string()),
95/// organization: Some(org),
96/// location: None,
97/// };
98///
99/// assert_eq!(network.cidr.as_deref(), Some("192.168.0.0/24"));
100/// ```
101#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
102pub struct NetworkInfo {
103 /// CIDR notation for the network (e.g., 192.168.1.0/24)
104 pub cidr: Option<String>,
105
106 /// Organization that owns this network
107 pub organization: Option<Organization>,
108
109 /// Location associated with this network
110 pub location: Option<Location>,
111}
112
113impl Organization {
114 /// Create a new Organization with the given name and ASN
115 ///
116 /// # Arguments
117 ///
118 /// * `name` - The name of the organization
119 /// * `asn` - The ASN (Autonomous System Number) of the organization
120 ///
121 /// # Returns
122 ///
123 /// A new Organization instance with the specified name and ASN
124 #[must_use]
125 pub fn new(name: Option<String>, asn: Option<String>) -> Self {
126 Organization {
127 name,
128 asn,
129 parent: None,
130 }
131 }
132
133 /// Set the parent organization
134 ///
135 /// # Arguments
136 ///
137 /// * `parent` - The parent organization
138 ///
139 /// # Returns
140 ///
141 /// Self with the parent organization set
142 #[must_use]
143 pub fn with_parent(mut self, parent: Organization) -> Self {
144 self.parent = Some(Box::new(parent));
145 self
146 }
147
148 /// Check if this organization has a parent
149 ///
150 /// # Returns
151 ///
152 /// `true` if this organization has a parent, `false` otherwise
153 #[must_use]
154 pub fn has_parent(&self) -> bool {
155 self.parent.is_some()
156 }
157
158 /// Get the ASN as a number if it exists
159 ///
160 /// # Returns
161 ///
162 /// The ASN as a u32 if it exists and can be parsed as a number,
163 /// or None if the ASN is not set or cannot be parsed
164 #[must_use]
165 pub fn get_asn_number(&self) -> Option<u32> {
166 self.asn.as_ref().and_then(|asn| asn.parse::<u32>().ok())
167 }
168}
169
170/// ASN (Autonomous System Number) information
171///
172/// Contains detailed information about an Autonomous System,
173/// including its identifier number, owning organization, and location.
174///
175/// # Examples
176///
177/// ```
178/// use gooty_proxy::inspection::AutonomousSystem;
179///
180/// // Create a new AutonomousSystem
181/// let asn = AutonomousSystem {
182/// number: 15169,
183/// organization: Some("Google LLC".to_string()),
184/// country: Some("US".to_string()),
185/// description: Some("Google Global LLC".to_string()),
186/// };
187///
188/// assert_eq!(asn.number, 15169);
189/// assert_eq!(asn.organization.as_deref(), Some("Google LLC"));
190/// ```
191#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
192pub struct AutonomousSystem {
193 /// The ASN number
194 pub number: u32,
195
196 /// The organization that owns this ASN
197 pub organization: Option<String>,
198
199 /// The country where this ASN is registered
200 pub country: Option<String>,
201
202 /// The description or name of the ASN
203 pub description: Option<String>,
204}
205
206/// Service for looking up ASN and organization information
207///
208/// This service provides methods for retrieving ownership information
209/// for IP addresses, including the organization, ASN, and network details.
210/// It uses IP geolocation and ASN lookup services to gather this data.
211///
212/// # Examples
213///
214/// ```no_run
215/// use std::net::{IpAddr, Ipv4Addr};
216/// use gooty_proxy::inspection::OwnershipLookup;
217///
218/// #[tokio::main]
219/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
220/// let lookup = OwnershipLookup::new();
221///
222/// // Lookup ASN for an IP
223/// let ip = IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8));
224/// let asn = lookup.lookup_asn(&ip).await?;
225///
226/// println!("ASN: {:?}", asn);
227///
228/// // Lookup organization information
229/// let org = lookup.lookup_organization(&ip).await?;
230/// if let Some(org) = org {
231/// println!("Organization: {:?}", org.name);
232/// println!("ASN: {:?}", org.asn);
233/// }
234///
235/// Ok(())
236/// }
237/// ```
238pub struct OwnershipLookup {
239 client: Client,
240}
241
242impl Default for OwnershipLookup {
243 fn default() -> Self {
244 Self::new()
245 }
246}
247
248impl OwnershipLookup {
249 /// Create a new ownership lookup service with default configuration
250 ///
251 /// Creates a new instance with a default HTTP client configuration,
252 /// including a 10-second timeout.
253 ///
254 /// # Returns
255 ///
256 /// A new `OwnershipLookup` instance
257 #[must_use]
258 pub fn new() -> Self {
259 let client = Client::builder()
260 .timeout(Duration::from_secs(10))
261 .build()
262 .unwrap_or_else(|_| Client::new());
263
264 OwnershipLookup { client }
265 }
266
267 /// Create a new ownership lookup service with a custom HTTP client
268 ///
269 /// # Arguments
270 ///
271 /// * `client` - A pre-configured HTTP client
272 ///
273 /// # Returns
274 ///
275 /// A new `OwnershipLookup` instance with the specified client
276 #[must_use]
277 pub fn with_client(client: Client) -> Self {
278 OwnershipLookup { client }
279 }
280
281 /// Lookup ASN information for an IP address
282 ///
283 /// # Arguments
284 ///
285 /// * `ip` - The IP address to lookup
286 ///
287 /// # Returns
288 ///
289 /// The ASN as a string if found, or None if not available
290 ///
291 /// # Errors
292 ///
293 /// Returns an error if:
294 /// * The request to the ASN lookup service fails
295 /// * The response cannot be parsed
296 /// * The service returns an error status code
297 pub async fn lookup_asn(&self, ip: &IpAddr) -> OwnershipResult<Option<String>> {
298 // Use ipinfo.io's free API to get ASN information
299 let url = format!("https://ipinfo.io/{ip}/json");
300
301 let response = self
302 .client
303 .get(&url)
304 .header("Accept", "application/json")
305 .send()
306 .await
307 .map_err(|e| OwnershipError::NetworkError(e.to_string()))?;
308
309 if !response.status().is_success() {
310 return match response.status().as_u16() {
311 404 => Err(OwnershipError::NotFound(ip.to_string())),
312 429 => Err(OwnershipError::RateLimited),
313 _ => Err(OwnershipError::ApiError(format!(
314 "Status {}",
315 response.status()
316 ))),
317 };
318 }
319
320 let data: serde_json::Value = response
321 .json()
322 .await
323 .map_err(|e| OwnershipError::ParseError(e.to_string()))?;
324
325 let asn = data.get("org").and_then(|v| v.as_str()).and_then(|org| {
326 // ASN is often prefixed in the org field like "AS15169 Google LLC"
327 let parts: Vec<&str> = org.split_whitespace().collect();
328 if !parts.is_empty() && parts[0].starts_with("AS") {
329 Some(parts[0].trim_start_matches("AS").to_string())
330 } else {
331 None
332 }
333 });
334
335 Ok(asn)
336 }
337
338 /// Lookup organization information for an IP address
339 ///
340 /// # Arguments
341 ///
342 /// * `ip` - The IP address to lookup
343 ///
344 /// # Returns
345 ///
346 /// An Organization if information is available, or None if not found
347 ///
348 /// # Errors
349 ///
350 /// Returns an error if:
351 /// * The request to the organization lookup service fails
352 /// * The response cannot be parsed
353 /// * The service returns an error status code
354 pub async fn lookup_organization(&self, ip: &IpAddr) -> OwnershipResult<Option<Organization>> {
355 // Use ipinfo.io's free API to get organization information
356 let url = format!("https://ipinfo.io/{ip}/json");
357
358 let response = self
359 .client
360 .get(&url)
361 .header("Accept", "application/json")
362 .send()
363 .await
364 .map_err(|e| OwnershipError::NetworkError(e.to_string()))?;
365
366 if !response.status().is_success() {
367 return match response.status().as_u16() {
368 404 => Err(OwnershipError::NotFound(ip.to_string())),
369 429 => Err(OwnershipError::RateLimited),
370 _ => Err(OwnershipError::ApiError(format!(
371 "Status {}",
372 response.status()
373 ))),
374 };
375 }
376
377 let data: serde_json::Value = response
378 .json()
379 .await
380 .map_err(|e| OwnershipError::ParseError(e.to_string()))?;
381
382 let org_str = data.get("org").and_then(|v| v.as_str());
383
384 if let Some(org_str) = org_str {
385 // Parse organization string like "AS15169 Google LLC"
386 let parts: Vec<&str> = org_str.splitn(2, ' ').collect();
387 let (asn, name) = if parts.len() == 2 && parts[0].starts_with("AS") {
388 (
389 Some(parts[0].trim_start_matches("AS").to_string()),
390 Some(parts[1].to_string()),
391 )
392 } else {
393 (None, Some(org_str.to_string()))
394 };
395
396 let org = Organization {
397 name,
398 asn,
399 parent: None,
400 };
401
402 Ok(Some(org))
403 } else {
404 Ok(None)
405 }
406 }
407
408 /// Try to find parent organizations and ownership chain
409 ///
410 /// Attempts to build a chain of organization ownership for the IP address.
411 /// In this simplified implementation, it returns just the immediate organization.
412 /// A more comprehensive implementation would follow ownership chains through
413 /// multiple data sources.
414 ///
415 /// # Arguments
416 ///
417 /// * `ip` - The IP address to lookup
418 ///
419 /// # Returns
420 ///
421 /// A vector of Organizations representing the ownership chain,
422 /// from direct owner to ultimate parent
423 ///
424 /// # Errors
425 ///
426 /// Returns an error if the organization lookup fails
427 ///
428 /// # Note
429 ///
430 /// This requires multiple API calls and might hit rate limits with free APIs
431 pub async fn lookup_ownership_chain(&self, ip: &IpAddr) -> OwnershipResult<Vec<Organization>> {
432 // This is a simplified implementation as full ownership chain lookup
433 // would require premium API access or multiple data sources.
434 // For now, we'll just return the immediate organization.
435
436 let org = self.lookup_organization(ip).await?;
437
438 match org {
439 Some(org) => Ok(vec![org]),
440 None => Ok(vec![]),
441 }
442 }
443
444 /// Lookup detailed information about an ASN
445 ///
446 /// # Arguments
447 ///
448 /// * `asn` - The ASN to lookup, with or without the "AS" prefix
449 ///
450 /// # Returns
451 ///
452 /// Detailed information about the ASN if available, or None if not found
453 ///
454 /// # Errors
455 ///
456 /// Returns an error if:
457 /// * The ASN is not a valid number
458 /// * The request to the ASN lookup service fails
459 /// * The response cannot be parsed
460 /// * The service returns an error status code
461 pub async fn lookup_asn_details(&self, asn: &str) -> OwnershipResult<Option<AutonomousSystem>> {
462 // Remove "AS" prefix if present
463 let asn_number = asn.trim_start_matches("AS");
464
465 // Ensure it's a valid number
466 let Ok(asn_num) = asn_number.parse::<u32>() else {
467 return Err(OwnershipError::ParseError(format!("Invalid ASN: {asn}")));
468 };
469
470 // Use ipinfo.io's free API to get ASN information
471 // Note: This is a simplified implementation as detailed ASN lookup
472 // typically requires a paid API or more specific data source
473 let url = format!("https://ipinfo.io/AS{asn_num}/json");
474
475 let response = self
476 .client
477 .get(&url)
478 .header("Accept", "application/json")
479 .send()
480 .await
481 .map_err(|e| OwnershipError::NetworkError(e.to_string()))?;
482
483 if !response.status().is_success() {
484 return match response.status().as_u16() {
485 404 => Err(OwnershipError::NotFound(asn.to_string())),
486 429 => Err(OwnershipError::RateLimited),
487 _ => Err(OwnershipError::ApiError(format!(
488 "Status {}",
489 response.status()
490 ))),
491 };
492 }
493
494 let data: serde_json::Value = response
495 .json()
496 .await
497 .map_err(|e| OwnershipError::ParseError(e.to_string()))?;
498
499 let org = data.get("name").and_then(|v| v.as_str()).map(String::from);
500 let country = data
501 .get("country")
502 .and_then(|v| v.as_str())
503 .map(String::from);
504 let description = data
505 .get("domain")
506 .and_then(|v| v.as_str())
507 .map(String::from);
508
509 // Only create an ASN if we have at least some information
510 if org.is_some() || country.is_some() || description.is_some() {
511 let asn_details = AutonomousSystem {
512 number: asn_num,
513 organization: org,
514 country,
515 description,
516 };
517
518 Ok(Some(asn_details))
519 } else {
520 Ok(None)
521 }
522 }
523}