1use std::borrow::Cow;
2use std::future::Future;
3use std::net::{IpAddr, SocketAddr};
4use std::pin::Pin;
5use std::str;
6use std::task::{Context, Poll};
7
8use futures_core::Stream;
9use futures_util::future::BoxFuture;
10use futures_util::{future, ready, stream};
11use http::{Response, Uri};
12use hyper::{
13 body::{self, Body, Buf},
14 client::{Builder, Client},
15};
16use pin_project_lite::pin_project;
17use thiserror::Error;
18use tracing::trace_span;
19use tracing_futures::Instrument;
20
21#[cfg(feature = "tokio-http-resolver")]
22use hyper::client::connect::{HttpConnector, HttpInfo};
23
24#[cfg(feature = "tokio-http-resolver")]
25type GaiResolver = hyper_system_resolver::system::Resolver;
26
27use crate::{Resolutions, Version};
28
29pub const ALL: &dyn crate::Resolver<'static> = &&[
34 #[cfg(feature = "ipify-org")]
35 HTTP_IPIFY_ORG,
36 #[cfg(feature = "whatismyipaddress-com")]
37 HTTP_WHATISMYIPADDRESS_COM,
38];
39
40#[cfg(feature = "ipify-org")]
42#[cfg_attr(docsrs, doc(cfg(feature = "ipify-org")))]
43pub const HTTP_IPIFY_ORG: &dyn crate::Resolver<'static> =
44 &Resolver::new_static("http://api.ipify.org", ExtractMethod::PlainText);
45
46#[cfg(feature = "whatismyipaddress-com")]
48#[cfg_attr(docsrs, doc(cfg(feature = "whatismyipaddress-com")))]
49pub const HTTP_WHATISMYIPADDRESS_COM: &dyn crate::Resolver<'static> =
50 &Resolver::new_static("http://bot.whatismyipaddress.com", ExtractMethod::PlainText);
51
52#[derive(Debug, Error)]
57pub enum Error {
58 #[error("{0}")]
60 Client(hyper::Error),
61 #[error("{0}")]
63 Uri(http::uri::InvalidUri),
64}
65
66#[derive(Debug, Clone)]
71pub struct Details {
72 uri: Uri,
73 server: SocketAddr,
74 method: ExtractMethod,
75}
76
77impl Details {
78 pub fn uri(&self) -> &Uri {
80 &self.uri
81 }
82
83 pub fn server(&self) -> SocketAddr {
85 self.server
86 }
87
88 pub fn extract_method(&self) -> ExtractMethod {
90 self.method
91 }
92}
93
94#[derive(Debug, Clone, Copy)]
96pub enum ExtractMethod {
97 PlainText,
99 StripDoubleQuotes,
101 ExtractJsonIpField,
105}
106
107#[derive(Debug, Clone)]
112pub struct Resolver<'r> {
113 uri: Cow<'r, str>,
114 method: ExtractMethod,
115}
116
117impl<'r> Resolver<'r> {
118 pub fn new<U>(uri: U, method: ExtractMethod) -> Self
120 where
121 U: Into<Cow<'r, str>>,
122 {
123 Self {
124 uri: uri.into(),
125 method,
126 }
127 }
128}
129
130impl Resolver<'static> {
131 #[must_use]
133 pub const fn new_static(uri: &'static str, method: ExtractMethod) -> Self {
134 Self {
135 uri: Cow::Borrowed(uri),
136 method,
137 }
138 }
139}
140
141pin_project! {
145 #[project = HttpResolutionsProj]
146 enum HttpResolutions<'r> {
147 HttpRequest {
148 #[pin]
149 response: BoxFuture<'r, Result<(IpAddr, crate::Details), crate::Error>>,
150 },
151 Done,
152 }
153}
154
155impl<'r> Stream for HttpResolutions<'r> {
156 type Item = Result<(IpAddr, crate::Details), crate::Error>;
157
158 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
159 match self.as_mut().project() {
160 HttpResolutionsProj::HttpRequest { response } => {
161 let response = ready!(response.poll(cx));
162 *self = HttpResolutions::Done;
163 Poll::Ready(Some(response))
164 }
165 HttpResolutionsProj::Done => Poll::Ready(None),
166 }
167 }
168}
169
170async fn resolve(
171 version: Version,
172 uri: Uri,
173 method: ExtractMethod,
174) -> Result<(IpAddr, crate::Details), crate::Error> {
175 let response = http_client(version)
176 .get(uri.clone())
177 .await
178 .map_err(Error::Client)?;
179 let server = remote_addr(&response);
181 let mut body = body::aggregate(response.into_body())
182 .await
183 .map_err(Error::Client)?;
184 let body = body.copy_to_bytes(body.remaining());
185 let body_str = str::from_utf8(body.as_ref())?;
186 let address_str = match method {
187 ExtractMethod::PlainText => body_str.trim(),
188 ExtractMethod::ExtractJsonIpField => extract_json_ip_field(body_str)?,
189 ExtractMethod::StripDoubleQuotes => body_str.trim().trim_matches('"'),
190 };
191 let address = address_str.parse()?;
192 let details = Box::new(Details {
193 uri,
194 server,
195 method,
196 });
197 Ok((address, crate::Details::from(details)))
198}
199
200impl<'r> crate::Resolver<'r> for Resolver<'r> {
201 fn resolve(&self, version: Version) -> Resolutions<'r> {
202 let method = self.method;
203 let uri: Uri = match self.uri.as_ref().parse() {
204 Ok(name) => name,
205 Err(err) => return Box::pin(stream::once(future::ready(Err(crate::Error::new(err))))),
206 };
207 let span = trace_span!("http resolver", ?version, ?method, %uri);
208 let resolutions = HttpResolutions::HttpRequest {
209 response: Box::pin(resolve(version, uri, method)),
210 };
211 Box::pin(resolutions.instrument(span))
212 }
213}
214
215fn extract_json_ip_field(s: &str) -> Result<&str, crate::Error> {
216 s.split_once(r#""ip":"#)
217 .and_then(|(_, after_prop)| after_prop.split('"').nth(1))
218 .ok_or(crate::Error::Addr)
219}
220
221#[cfg(feature = "tokio-http-resolver")]
225fn http_client(version: Version) -> Client<HttpConnector<GaiResolver>, Body> {
226 use dns_lookup::{AddrFamily, AddrInfoHints, SockType};
227 use hyper_system_resolver::system::System;
228 let hints = match version {
229 Version::V4 => AddrInfoHints {
230 address: AddrFamily::Inet.into(),
231 ..AddrInfoHints::default()
232 },
233 Version::V6 => AddrInfoHints {
234 address: AddrFamily::Inet6.into(),
235 ..AddrInfoHints::default()
236 },
237 Version::Any => AddrInfoHints {
238 socktype: SockType::Stream.into(),
239 ..AddrInfoHints::default()
240 },
241 };
242 let system = System {
243 addr_info_hints: Some(hints),
244 service: None,
245 };
246 let connector = HttpConnector::new_with_resolver(system.resolver());
247 Builder::default().build(connector)
248}
249
250#[cfg(feature = "tokio-http-resolver")]
251fn remote_addr(response: &Response<Body>) -> SocketAddr {
252 response
253 .extensions()
254 .get::<HttpInfo>()
255 .unwrap()
256 .remote_addr()
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn test_extract_json_ip_field() {
265 const VALID: &str = r#"{
266 "ip": "123.123.123.123",
267 }"#;
268
269 const INVALID: &str = r#"{
270 "ipp": "123.123.123.123",
271 }"#;
272
273 const VALID_INVALID: &str = r#"{
274 "ip": "123.123.123.123",
275 "ip": "321.321.321.321",
276 }"#;
277
278 assert_eq!(extract_json_ip_field(VALID).unwrap(), "123.123.123.123");
279 assert_eq!(
280 extract_json_ip_field(VALID_INVALID).unwrap(),
281 "123.123.123.123"
282 );
283 assert!(matches!(
284 extract_json_ip_field(INVALID).unwrap_err(),
285 crate::Error::Addr
286 ));
287 }
288}