1pub(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
18pub type ResponseParser = fn(&str) -> Option<IpAddr>;
20
21pub fn parse_plain_text(text: &str) -> Option<IpAddr> {
23 IpAddr::from_str(text.trim()).ok()
24}
25
26pub 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#[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 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 pub fn with_parser(mut self, parser: ResponseParser) -> Self {
65 self.parser = parser;
66 self
67 }
68
69 pub fn with_v6_url(mut self, url: impl Into<String>) -> Self {
71 self.url_v6 = Some(url.into());
72 self
73 }
74
75 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 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}