Skip to main content

ip_discovery/http/
mod.rs

1//! HTTP/HTTPS protocol implementation for public IP detection
2//!
3//! Uses various HTTP-based IP detection services.
4
5pub(crate) mod providers;
6
7pub use providers::{default_providers, provider_names};
8
9use crate::error::ProviderError;
10use crate::provider::Provider;
11use crate::types::{IpVersion, Protocol};
12use async_trait::async_trait;
13use reqwest::Client;
14use std::net::IpAddr;
15use std::str::FromStr;
16use tracing::debug;
17
18/// Response parser function type
19pub type ResponseParser = fn(&str) -> Option<IpAddr>;
20
21/// Parse plain text IP response
22pub fn parse_plain_text(text: &str) -> Option<IpAddr> {
23    IpAddr::from_str(text.trim()).ok()
24}
25
26/// Parse Cloudflare trace response (key=value format)
27pub fn parse_cloudflare_trace(text: &str) -> Option<IpAddr> {
28    for line in text.lines() {
29        if let Some(ip_str) = line.strip_prefix("ip=") {
30            return IpAddr::from_str(ip_str.trim()).ok();
31        }
32    }
33    None
34}
35
36/// HTTP provider configuration
37#[derive(Clone)]
38pub struct HttpProvider {
39    name: String,
40    url_v4: Option<String>,
41    url_v6: Option<String>,
42    parser: ResponseParser,
43    client: Client,
44}
45
46impl HttpProvider {
47    /// Create a new HTTP provider (plain text response)
48    pub fn new(name: impl Into<String>, url: impl Into<String>) -> Self {
49        let client = Client::builder()
50            .user_agent(concat!("ip-discovery/", env!("CARGO_PKG_VERSION")))
51            .build()
52            .unwrap_or_default();
53
54        Self {
55            name: name.into(),
56            url_v4: Some(url.into()),
57            url_v6: None,
58            parser: parse_plain_text,
59            client,
60        }
61    }
62
63    /// Set custom response parser
64    pub fn with_parser(mut self, parser: ResponseParser) -> Self {
65        self.parser = parser;
66        self
67    }
68
69    /// Set IPv6 URL
70    pub fn with_v6_url(mut self, url: impl Into<String>) -> Self {
71        self.url_v6 = Some(url.into());
72        self
73    }
74
75    /// Get URL for IP version
76    fn get_url(&self, version: IpVersion) -> Option<&str> {
77        match version {
78            IpVersion::V6 => self.url_v6.as_deref().or(self.url_v4.as_deref()),
79            _ => self.url_v4.as_deref(),
80        }
81    }
82
83    /// Fetch IP from URL
84    async fn fetch(&self, version: IpVersion) -> Result<IpAddr, ProviderError> {
85        let url = self
86            .get_url(version)
87            .ok_or_else(|| ProviderError::message(&self.name, "no URL for IP version"))?;
88
89        debug!(provider = %self.name, url = %url, "fetching IP via HTTP");
90
91        let response = self
92            .client
93            .get(url)
94            .send()
95            .await
96            .map_err(|e| ProviderError::new(&self.name, e))?;
97
98        if !response.status().is_success() {
99            return Err(ProviderError::message(
100                &self.name,
101                format!("HTTP error: {}", response.status()),
102            ));
103        }
104
105        let text = response
106            .text()
107            .await
108            .map_err(|e| ProviderError::new(&self.name, e))?;
109
110        (self.parser)(&text)
111            .ok_or_else(|| ProviderError::message(&self.name, "failed to parse response"))
112    }
113}
114
115impl std::fmt::Debug for HttpProvider {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        f.debug_struct("HttpProvider")
118            .field("name", &self.name)
119            .field("url_v4", &self.url_v4)
120            .field("url_v6", &self.url_v6)
121            .finish()
122    }
123}
124
125#[async_trait]
126impl Provider for HttpProvider {
127    fn name(&self) -> &str {
128        &self.name
129    }
130
131    fn protocol(&self) -> Protocol {
132        Protocol::Http
133    }
134
135    fn supports_v4(&self) -> bool {
136        self.url_v4.is_some()
137    }
138
139    fn supports_v6(&self) -> bool {
140        self.url_v6.is_some()
141    }
142
143    async fn get_ip(&self, version: IpVersion) -> Result<IpAddr, ProviderError> {
144        self.fetch(version).await
145    }
146}