myip_foo/
lib.rs

1//! # myip-foo
2//!
3//! Official Rust client for [myip.foo](https://myip.foo) - a free, privacy-focused IP lookup API.
4//!
5//! ## Features
6//!
7//! - Async/await support with tokio
8//! - Full type definitions with serde
9//! - Dual-stack IPv4/IPv6 support
10//! - Connection type detection
11//! - No API key required
12//!
13//! ## Quick Start
14//!
15//! ```rust,no_run
16//! use myip_foo::{get_ip, get_ip_data};
17//!
18//! #[tokio::main]
19//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
20//!     // Get plain IP
21//!     let ip = get_ip().await?;
22//!     println!("IP: {}", ip);
23//!
24//!     // Get full data
25//!     let data = get_ip_data().await?;
26//!     println!("City: {}", data.location.city);
27//!
28//!     Ok(())
29//! }
30//! ```
31
32use reqwest::Client;
33use serde::{Deserialize, Serialize};
34use std::time::Duration;
35
36const BASE_URL: &str = "https://myip.foo";
37const IPV4_URL: &str = "https://ipv4.myip.foo";
38const IPV6_URL: &str = "https://ipv6.myip.foo";
39
40/// Location information
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct Location {
44    pub country: String,
45    pub city: String,
46    pub region: String,
47    pub postal_code: String,
48    pub timezone: String,
49    pub latitude: String,
50    pub longitude: String,
51}
52
53/// Network information
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct Network {
56    pub asn: i64,
57    pub isp: String,
58}
59
60/// Cloudflare information
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Cloudflare {
63    pub colo: String,
64    pub ray: String,
65}
66
67/// Full IP data including geolocation
68#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct IpData {
71    pub ip: String,
72    #[serde(rename = "type")]
73    pub ip_type: String,
74    pub hostname: Option<String>,
75    pub connection_type: Option<String>,
76    pub location: Location,
77    pub network: Network,
78    pub cloudflare: Cloudflare,
79}
80
81/// Dual-stack IPv4/IPv6 data
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct DualStackData {
84    pub ipv4: Option<String>,
85    pub ipv6: Option<String>,
86}
87
88/// Connection type data
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ConnectionTypeData {
91    pub ip: String,
92    #[serde(rename = "type")]
93    pub connection_type: String,
94}
95
96/// IP response from dual-stack endpoints
97#[derive(Debug, Clone, Serialize, Deserialize)]
98struct IpResponse {
99    ip: String,
100}
101
102/// Error type for myip-foo operations
103#[derive(Debug)]
104pub enum Error {
105    Request(reqwest::Error),
106    Parse(serde_json::Error),
107}
108
109impl std::fmt::Display for Error {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        match self {
112            Error::Request(e) => write!(f, "Request error: {}", e),
113            Error::Parse(e) => write!(f, "Parse error: {}", e),
114        }
115    }
116}
117
118impl std::error::Error for Error {}
119
120impl From<reqwest::Error> for Error {
121    fn from(err: reqwest::Error) -> Self {
122        Error::Request(err)
123    }
124}
125
126impl From<serde_json::Error> for Error {
127    fn from(err: serde_json::Error) -> Self {
128        Error::Parse(err)
129    }
130}
131
132/// Result type for myip-foo operations
133pub type Result<T> = std::result::Result<T, Error>;
134
135fn create_client() -> reqwest::Result<Client> {
136    Client::builder()
137        .user_agent("myip-foo/1.0.0")
138        .timeout(Duration::from_secs(10))
139        .build()
140}
141
142/// Get your IP address as plain text.
143///
144/// # Example
145///
146/// ```rust,no_run
147/// # #[tokio::main]
148/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
149/// let ip = myip_foo::get_ip().await?;
150/// println!("My IP: {}", ip);
151/// # Ok(())
152/// # }
153/// ```
154pub async fn get_ip() -> Result<String> {
155    let client = create_client()?;
156    let response = client
157        .get(format!("{}/plain", BASE_URL))
158        .send()
159        .await?
160        .text()
161        .await?;
162    Ok(response.trim().to_string())
163}
164
165/// Get full IP data including geolocation.
166///
167/// # Example
168///
169/// ```rust,no_run
170/// # #[tokio::main]
171/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
172/// let data = myip_foo::get_ip_data().await?;
173/// println!("IP: {}", data.ip);
174/// println!("City: {}", data.location.city);
175/// println!("ISP: {}", data.network.isp);
176/// # Ok(())
177/// # }
178/// ```
179pub async fn get_ip_data() -> Result<IpData> {
180    let client = create_client()?;
181    let response = client
182        .get(format!("{}/api", BASE_URL))
183        .send()
184        .await?
185        .json()
186        .await?;
187    Ok(response)
188}
189
190/// Get both IPv4 and IPv6 addresses.
191///
192/// Uses dedicated endpoints that bypass Cloudflare's dual-stack routing.
193///
194/// # Example
195///
196/// ```rust,no_run
197/// # #[tokio::main]
198/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
199/// let dual = myip_foo::get_dual_stack().await?;
200/// if let Some(ipv4) = dual.ipv4 {
201///     println!("IPv4: {}", ipv4);
202/// }
203/// if let Some(ipv6) = dual.ipv6 {
204///     println!("IPv6: {}", ipv6);
205/// }
206/// # Ok(())
207/// # }
208/// ```
209pub async fn get_dual_stack() -> Result<DualStackData> {
210    let client = Client::builder()
211        .user_agent("myip-foo/1.0.0")
212        .timeout(Duration::from_secs(5))
213        .build()?;
214
215    let ipv4 = match client.get(format!("{}/ip", IPV4_URL)).send().await {
216        Ok(resp) => match resp.json::<IpResponse>().await {
217            Ok(data) => Some(data.ip),
218            Err(_) => None,
219        },
220        Err(_) => None,
221    };
222
223    let ipv6 = match client.get(format!("{}/ip", IPV6_URL)).send().await {
224        Ok(resp) => match resp.json::<IpResponse>().await {
225            Ok(data) => Some(data.ip),
226            Err(_) => None,
227        },
228        Err(_) => None,
229    };
230
231    Ok(DualStackData { ipv4, ipv6 })
232}
233
234/// Get connection type (residential, vpn, datacenter).
235///
236/// # Example
237///
238/// ```rust,no_run
239/// # #[tokio::main]
240/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
241/// let conn = myip_foo::get_connection_type().await?;
242/// println!("Type: {}", conn.connection_type);
243/// # Ok(())
244/// # }
245/// ```
246pub async fn get_connection_type() -> Result<ConnectionTypeData> {
247    let client = create_client()?;
248    let response = client
249        .get(format!("{}/api/connection-type", BASE_URL))
250        .send()
251        .await?
252        .json()
253        .await?;
254    Ok(response)
255}
256
257/// Get all HTTP headers as seen by the server.
258///
259/// # Example
260///
261/// ```rust,no_run
262/// # #[tokio::main]
263/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
264/// let headers = myip_foo::get_headers().await?;
265/// for (key, value) in headers {
266///     println!("{}: {}", key, value);
267/// }
268/// # Ok(())
269/// # }
270/// ```
271pub async fn get_headers() -> Result<std::collections::HashMap<String, String>> {
272    let client = create_client()?;
273    let response = client
274        .get(format!("{}/headers", BASE_URL))
275        .send()
276        .await?
277        .json()
278        .await?;
279    Ok(response)
280}
281
282/// Get your user agent string.
283///
284/// # Example
285///
286/// ```rust,no_run
287/// # #[tokio::main]
288/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
289/// let ua = myip_foo::get_user_agent().await?;
290/// println!("User-Agent: {}", ua);
291/// # Ok(())
292/// # }
293/// ```
294pub async fn get_user_agent() -> Result<String> {
295    let client = create_client()?;
296    let response: serde_json::Value = client
297        .get(format!("{}/user-agent", BASE_URL))
298        .send()
299        .await?
300        .json()
301        .await?;
302
303    Ok(response["userAgent"]
304        .as_str()
305        .unwrap_or("")
306        .to_string())
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[tokio::test]
314    async fn test_get_ip() {
315        let result = get_ip().await;
316        assert!(result.is_ok());
317        let ip = result.unwrap();
318        assert!(!ip.is_empty());
319    }
320}