public_ip/
http.rs

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
29///////////////////////////////////////////////////////////////////////////////
30// Hardcoded resolvers
31
32/// All builtin HTTP resolvers.
33pub 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/// `http://api.ipify.org` HTTP resolver options
41#[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/// `http://bot.whatismyipaddress.com` HTTP resolver options
47#[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///////////////////////////////////////////////////////////////////////////////
53// Error
54
55/// HTTP resolver error
56#[derive(Debug, Error)]
57pub enum Error {
58    /// Client error.
59    #[error("{0}")]
60    Client(hyper::Error),
61    /// URI parsing error.
62    #[error("{0}")]
63    Uri(http::uri::InvalidUri),
64}
65
66///////////////////////////////////////////////////////////////////////////////
67// Details & options
68
69/// A resolution produced from a HTTP resolver
70#[derive(Debug, Clone)]
71pub struct Details {
72    uri: Uri,
73    server: SocketAddr,
74    method: ExtractMethod,
75}
76
77impl Details {
78    /// URI used in the resolution of the associated IP address
79    pub fn uri(&self) -> &Uri {
80        &self.uri
81    }
82
83    /// HTTP server used in the resolution of our IP address.
84    pub fn server(&self) -> SocketAddr {
85        self.server
86    }
87
88    /// The extract method used in the resolution of the associated IP address
89    pub fn extract_method(&self) -> ExtractMethod {
90        self.method
91    }
92}
93
94/// Method used to extract an IP address from a http response
95#[derive(Debug, Clone, Copy)]
96pub enum ExtractMethod {
97    /// Parses the body with whitespace trimmed as the IP address.
98    PlainText,
99    /// Parses the body with double quotes and whitespace trimmed as the IP address.
100    StripDoubleQuotes,
101    /// Parses the value of the JSON property `"ip"` within the body as the IP address.
102    ///
103    /// Note this method does not validate the JSON.
104    ExtractJsonIpField,
105}
106
107///////////////////////////////////////////////////////////////////////////////
108// Resolver
109
110/// Options to build a HTTP resolver
111#[derive(Debug, Clone)]
112pub struct Resolver<'r> {
113    uri: Cow<'r, str>,
114    method: ExtractMethod,
115}
116
117impl<'r> Resolver<'r> {
118    /// Create new HTTP resolver options
119    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    /// Create new HTTP resolver options from static
132    #[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
141///////////////////////////////////////////////////////////////////////////////
142// Resolutions
143
144pin_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    // TODO
180    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///////////////////////////////////////////////////////////////////////////////
222// Client
223
224#[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}