Skip to main content

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}