1#![deny(
63 missing_docs,
64 trivial_numeric_casts,
65 unused_extern_crates,
66 unused_import_braces,
67 variant_size_differences,
68 unused_features,
69 unused_results,
70 warnings
71)]
72
73use async_trait::async_trait;
74use auto_impl::auto_impl;
75#[cfg(not(target_family = "wasm"))]
76use reqwest::Client;
77use std::fmt::Formatter;
78use std::net::IpAddr;
79#[cfg(not(target_family = "wasm"))]
80use std::net::SocketAddr;
81use std::str::FromStr;
82
83const URL_V6: &str = "http://api64.ipify.org";
85const URL_V6_1: &str = "http://ident.me";
88const URL_V6_2: &str = "http://v4v6.ipv6-test.com/api/myip.php";
91#[derive(Debug, Clone)]
94#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
95pub struct IpAddressInfo {
97 pub internal_ip: IpAddr,
99 pub external_ipv6: Option<IpAddr>,
101}
102
103impl IpAddressInfo {
104 pub fn localhost() -> Self {
106 let localhost = IpAddr::from_str("127.0.0.1").unwrap();
107 Self {
108 internal_ip: localhost,
109 external_ipv6: None,
110 }
111 }
112}
113
114pub async fn get_all_multi_concurrent<T: AsyncHttpGetClient>(
116 client: Option<T>,
117) -> Result<IpAddressInfo, IpRetrieveError> {
118 get_all_multi_concurrent_from(client, &[URL_V6, URL_V6_1, URL_V6_2]).await
119}
120
121pub async fn get_all_multi_concurrent_from<T: AsyncHttpGetClient>(
123 client: Option<T>,
124 v6_addrs: &[&str],
125) -> Result<IpAddressInfo, IpRetrieveError> {
126 let client = client.map(|client| Box::new(client) as Box<dyn AsyncHttpGetClient>);
127 let client = &client.unwrap_or_else(|| Box::new(get_default_client()));
128 let internal_ipv4_future = get_internal_ip(false);
129 let external_ipv6_future = futures::future::select_ok(
130 v6_addrs
131 .iter()
132 .map(|addr| Box::pin(get_ip_from(Some(client), addr)))
133 .collect::<Vec<_>>(),
134 );
135
136 let (res0, res2) = citadel_io::tokio::join!(internal_ipv4_future, external_ipv6_future);
137 let internal_ipv4 =
138 res0.ok_or_else(|| IpRetrieveError::Error("Could not obtain internal IPv4".to_string()))?;
139 let external_ipv6 = res2.ok().map(|r| r.0);
140
141 Ok(IpAddressInfo {
142 internal_ip: internal_ipv4,
143 external_ipv6,
144 })
145}
146
147pub async fn get_all<T: AsyncHttpGetClient>(
149 client: Option<T>,
150) -> Result<IpAddressInfo, IpRetrieveError> {
151 get_all_from(client, URL_V6).await
152}
153
154pub async fn get_all_from<T: AsyncHttpGetClient>(
156 client: Option<T>,
157 v6_addr: &str,
158) -> Result<IpAddressInfo, IpRetrieveError> {
159 let client = client
160 .map(|client| Box::new(client) as Box<dyn AsyncHttpGetClient>)
161 .unwrap_or_else(|| Box::new(get_default_client()));
162 let internal_ipv4_future = get_internal_ip(false);
163 let external_ipv6_future = get_ip_from(Some(client), v6_addr);
164 let (res0, res2) = citadel_io::tokio::join!(internal_ipv4_future, external_ipv6_future);
165 let internal_ipv4 =
166 res0.ok_or_else(|| IpRetrieveError::Error("Could not obtain internal IPv4".to_string()))?;
167 let external_ipv6 = res2.ok();
168
169 Ok(IpAddressInfo {
170 internal_ip: internal_ipv4,
171 external_ipv6,
172 })
173}
174
175pub async fn get_ip_from<T: AsyncHttpGetClient>(
181 client: Option<T>,
182 addr: &str,
183) -> Result<IpAddr, IpRetrieveError> {
184 let client = client
185 .map(|client| Box::new(client) as Box<dyn AsyncHttpGetClient>)
186 .unwrap_or_else(|| Box::new(get_default_client()));
187
188 let text = client.get(addr).await?;
189 IpAddr::from_str(text.as_str()).map_err(|err| IpRetrieveError::Error(err.to_string()))
190}
191
192pub async fn get_internal_ip(ipv6: bool) -> Option<IpAddr> {
194 if ipv6 {
195 get_internal_ipv6().await
196 } else {
197 get_internal_ipv4().await
198 }
199}
200
201#[cfg(not(target_family = "wasm"))]
202pub async fn get_internal_ipv4() -> Option<IpAddr> {
204 let socket = citadel_io::tokio::net::UdpSocket::bind(addr("0.0.0.0:0")?)
205 .await
206 .ok()?;
207 socket.connect(addr("8.8.8.8:80")?).await.ok()?;
208 socket.local_addr().ok().map(|sck| sck.ip())
209}
210
211#[cfg(target_family = "wasm")]
212async fn get_internal_ipv4() -> Option<IpAddr> {
213 None
214}
215
216#[cfg(not(target_family = "wasm"))]
217async fn get_internal_ipv6() -> Option<IpAddr> {
218 let socket = citadel_io::tokio::net::UdpSocket::bind(addr("[::]:0")?)
219 .await
220 .ok()?;
221 socket
222 .connect(addr("[2001:4860:4860::8888]:80")?)
223 .await
224 .ok()?;
225 socket.local_addr().ok().map(|sck| sck.ip())
226}
227
228#[cfg(target_family = "wasm")]
229async fn get_internal_ipv6() -> Option<IpAddr> {
230 None
231}
232
233#[cfg(not(target_family = "wasm"))]
234fn addr(addr: &str) -> Option<SocketAddr> {
235 SocketAddr::from_str(addr).ok()
236}
237
238#[cfg(not(target_family = "wasm"))]
239pub fn get_default_client() -> Client {
241 Client::builder().tcp_nodelay(true).build().unwrap()
242}
243#[cfg(target_family = "wasm")]
244fn get_default_client() -> UreqClient {
246 UreqClient
247}
248
249#[derive(Debug)]
251pub enum IpRetrieveError {
252 Error(String),
254}
255
256impl std::fmt::Display for IpRetrieveError {
257 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
258 match self {
259 IpRetrieveError::Error(err) => write!(f, "{}", err),
260 }
261 }
262}
263
264#[async_trait]
265#[auto_impl(Box, &)]
266pub trait AsyncHttpGetClient: Send + Sync {
268 async fn get(&self, addr: &str) -> Result<String, IpRetrieveError>;
270}
271
272#[cfg(not(target_family = "wasm"))]
273#[async_trait]
274impl AsyncHttpGetClient for Client {
275 async fn get(&self, addr: &str) -> Result<String, IpRetrieveError> {
276 let resp = self
277 .get(addr)
278 .send()
279 .await
280 .map_err(|err| IpRetrieveError::Error(err.to_string()))?;
281
282 resp.text()
283 .await
284 .map_err(|err| IpRetrieveError::Error(err.to_string()))
285 }
286}
287
288#[async_trait]
289impl AsyncHttpGetClient for () {
290 async fn get(&self, _addr: &str) -> Result<String, IpRetrieveError> {
291 unimplemented!("Stub implementation for AsyncHttpGetClient")
292 }
293}
294
295#[cfg(target_family = "wasm")]
296pub struct UreqClient;
298
299#[cfg(target_family = "wasm")]
300#[async_trait]
301impl AsyncHttpGetClient for UreqClient {
302 async fn get(&self, addr: &str) -> Result<String, IpRetrieveError> {
303 let addr = addr.to_string();
304 citadel_io::tokio::task::spawn_blocking(move || {
305 ureq::get(&addr)
306 .call()
307 .map_err(|err| IpRetrieveError::Error(err.to_string()))?
308 .into_string()
309 .map_err(|err| IpRetrieveError::Error(err.to_string()))
310 })
311 .await
312 .map_err(|err| IpRetrieveError::Error(err.to_string()))?
313 }
314}