use std::borrow::Cow;
use std::future::Future;
use std::net::{IpAddr, SocketAddr};
use std::pin::Pin;
use std::str;
use std::task::{Context, Poll};
use futures_core::Stream;
use futures_util::future::BoxFuture;
use futures_util::{future, ready, stream};
use http::{Response, Uri};
use hyper::{
body::{self, Body, Buf},
client::{Builder, Client},
};
use pin_project_lite::pin_project;
use thiserror::Error;
use tracing::trace_span;
use tracing_futures::Instrument;
#[cfg(feature = "tokio-http-resolver")]
use hyper::client::connect::{HttpConnector, HttpInfo};
#[cfg(feature = "tokio-http-resolver")]
type GaiResolver = hyper_system_resolver::system::Resolver;
use crate::{Resolutions, Version};
pub const ALL: &dyn crate::Resolver<'static> = &&[
#[cfg(feature = "ipify-org")]
HTTP_IPIFY_ORG,
#[cfg(feature = "whatismyipaddress-com")]
HTTP_WHATISMYIPADDRESS_COM,
];
#[cfg(feature = "ipify-org")]
#[cfg_attr(docsrs, doc(cfg(feature = "ipify-org")))]
pub const HTTP_IPIFY_ORG: &dyn crate::Resolver<'static> =
&Resolver::new_static("http://api.ipify.org", ExtractMethod::PlainText);
#[cfg(feature = "whatismyipaddress-com")]
#[cfg_attr(docsrs, doc(cfg(feature = "whatismyipaddress-com")))]
pub const HTTP_WHATISMYIPADDRESS_COM: &dyn crate::Resolver<'static> =
&Resolver::new_static("http://bot.whatismyipaddress.com", ExtractMethod::PlainText);
#[derive(Debug, Error)]
pub enum Error {
#[error("{0}")]
Client(hyper::Error),
#[error("{0}")]
Uri(http::uri::InvalidUri),
}
#[derive(Debug, Clone)]
pub struct Details {
uri: Uri,
server: SocketAddr,
method: ExtractMethod,
}
impl Details {
pub fn uri(&self) -> &Uri {
&self.uri
}
pub fn server(&self) -> SocketAddr {
self.server
}
pub fn extract_method(&self) -> ExtractMethod {
self.method
}
}
#[derive(Debug, Clone, Copy)]
pub enum ExtractMethod {
PlainText,
StripDoubleQuotes,
ExtractJsonIpField,
}
#[derive(Debug, Clone)]
pub struct Resolver<'r> {
uri: Cow<'r, str>,
method: ExtractMethod,
}
impl<'r> Resolver<'r> {
pub fn new<U>(uri: U, method: ExtractMethod) -> Self
where
U: Into<Cow<'r, str>>,
{
Self {
uri: uri.into(),
method,
}
}
}
impl Resolver<'static> {
#[must_use]
pub const fn new_static(uri: &'static str, method: ExtractMethod) -> Self {
Self {
uri: Cow::Borrowed(uri),
method,
}
}
}
pin_project! {
#[project = HttpResolutionsProj]
enum HttpResolutions<'r> {
HttpRequest {
#[pin]
response: BoxFuture<'r, Result<(IpAddr, crate::Details), crate::Error>>,
},
Done,
}
}
impl<'r> Stream for HttpResolutions<'r> {
type Item = Result<(IpAddr, crate::Details), crate::Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match self.as_mut().project() {
HttpResolutionsProj::HttpRequest { response } => {
let response = ready!(response.poll(cx));
*self = HttpResolutions::Done;
Poll::Ready(Some(response))
}
HttpResolutionsProj::Done => Poll::Ready(None),
}
}
}
async fn resolve(
version: Version,
uri: Uri,
method: ExtractMethod,
) -> Result<(IpAddr, crate::Details), crate::Error> {
let response = http_client(version)
.get(uri.clone())
.await
.map_err(Error::Client)?;
let server = remote_addr(&response);
let mut body = body::aggregate(response.into_body())
.await
.map_err(Error::Client)?;
let body = body.copy_to_bytes(body.remaining());
let body_str = str::from_utf8(body.as_ref())?;
let address_str = match method {
ExtractMethod::PlainText => body_str.trim(),
ExtractMethod::ExtractJsonIpField => extract_json_ip_field(body_str)?,
ExtractMethod::StripDoubleQuotes => body_str.trim().trim_matches('"'),
};
let address = address_str.parse()?;
let details = Box::new(Details {
uri,
server,
method,
});
Ok((address, crate::Details::from(details)))
}
impl<'r> crate::Resolver<'r> for Resolver<'r> {
fn resolve(&self, version: Version) -> Resolutions<'r> {
let method = self.method;
let uri: Uri = match self.uri.as_ref().parse() {
Ok(name) => name,
Err(err) => return Box::pin(stream::once(future::ready(Err(crate::Error::new(err))))),
};
let span = trace_span!("http resolver", ?version, ?method, %uri);
let resolutions = HttpResolutions::HttpRequest {
response: Box::pin(resolve(version, uri, method)),
};
Box::pin(resolutions.instrument(span))
}
}
fn extract_json_ip_field(s: &str) -> Result<&str, crate::Error> {
s.split_once(r#""ip":"#)
.and_then(|(_, after_prop)| after_prop.split('"').nth(1))
.ok_or(crate::Error::Addr)
}
#[cfg(feature = "tokio-http-resolver")]
fn http_client(version: Version) -> Client<HttpConnector<GaiResolver>, Body> {
use dns_lookup::{AddrFamily, AddrInfoHints, SockType};
use hyper_system_resolver::system::System;
let hints = match version {
Version::V4 => AddrInfoHints {
address: AddrFamily::Inet.into(),
..AddrInfoHints::default()
},
Version::V6 => AddrInfoHints {
address: AddrFamily::Inet6.into(),
..AddrInfoHints::default()
},
Version::Any => AddrInfoHints {
socktype: SockType::Stream.into(),
..AddrInfoHints::default()
},
};
let system = System {
addr_info_hints: Some(hints),
service: None,
};
let connector = HttpConnector::new_with_resolver(system.resolver());
Builder::default().build(connector)
}
#[cfg(feature = "tokio-http-resolver")]
fn remote_addr(response: &Response<Body>) -> SocketAddr {
response
.extensions()
.get::<HttpInfo>()
.unwrap()
.remote_addr()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_json_ip_field() {
const VALID: &str = r#"{
"ip": "123.123.123.123",
}"#;
const INVALID: &str = r#"{
"ipp": "123.123.123.123",
}"#;
const VALID_INVALID: &str = r#"{
"ip": "123.123.123.123",
"ip": "321.321.321.321",
}"#;
assert_eq!(extract_json_ip_field(VALID).unwrap(), "123.123.123.123");
assert_eq!(
extract_json_ip_field(VALID_INVALID).unwrap(),
"123.123.123.123"
);
assert!(matches!(
extract_json_ip_field(INVALID).unwrap_err(),
crate::Error::Addr
));
}
}