From 7928ed07a7c1309c01011d50b42fc3c1b08bbe2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=9C=AA=E6=9D=A5?= Date: Thu, 27 Feb 2025 17:22:55 +0800 Subject: [PATCH] =?UTF-8?q?DNS=20cache=E5=A4=9Aclient=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E3=80=81DOH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 徐未来 --- ylong_http_client/Cargo.toml | 10 + ylong_http_client/examples/async_http_dns.rs | 52 +++ ylong_http_client/examples/async_http_doh.rs | 54 +++ .../src/async_impl/connector/mod.rs | 4 + .../src/async_impl/dns/default.rs | 254 ++++++++++ ylong_http_client/src/async_impl/dns/doh.rs | 435 ++++++++++++++++++ ylong_http_client/src/async_impl/dns/mod.rs | 8 +- .../src/async_impl/dns/resolver.rs | 256 ++++------- ylong_http_client/src/async_impl/mod.rs | 2 + ylong_http_client/src/lib.rs | 3 +- 10 files changed, 914 insertions(+), 164 deletions(-) create mode 100644 ylong_http_client/examples/async_http_dns.rs create mode 100644 ylong_http_client/examples/async_http_doh.rs create mode 100644 ylong_http_client/src/async_impl/dns/default.rs create mode 100644 ylong_http_client/src/async_impl/dns/doh.rs diff --git a/ylong_http_client/Cargo.toml b/ylong_http_client/Cargo.toml index 108d7f7..8e0c79b 100644 --- a/ylong_http_client/Cargo.toml +++ b/ylong_http_client/Cargo.toml @@ -55,6 +55,16 @@ name = "async_http" path = "examples/async_http.rs" required-features = ["async", "http1_1", "ylong_base"] +[[example]] +name = "async_http_dns" +path = "examples/async_http_dns.rs" +required-features = ["async", "http1_1", "ylong_base"] + +[[example]] +name = "async_http_doh" +path = "examples/async_http_doh.rs" +required-features = ["async", "http1_1", "ylong_base", "__c_openssl"] + [[example]] name = "async_http_multi" path = "examples/async_http_multi.rs" diff --git a/ylong_http_client/examples/async_http_dns.rs b/ylong_http_client/examples/async_http_dns.rs new file mode 100644 index 0000000..cb3704c --- /dev/null +++ b/ylong_http_client/examples/async_http_dns.rs @@ -0,0 +1,52 @@ +// Copyright (c) 2025 Huawei Device Co., Ltd. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! This is a simple asynchronous HTTP client example using the +//! ylong_http_client crate. It demonstrates creating a client, making a +//! request, and reading the response asynchronously. + +use ylong_http_client::async_impl::{Body, Client, DefaultDnsResolver, Downloader, Request}; +use ylong_http_client::HttpClientError; + +fn main() -> Result<(), HttpClientError> { + let handle = ylong_runtime::spawn(async move { + connect().await.unwrap(); + }); + + let _ = ylong_runtime::block_on(handle); + Ok(()) +} + +async fn connect() -> Result<(), HttpClientError> { + // Creates a `Default Dns Resolver`. + let default_dns_resolver = DefaultDnsResolver::new(); + + // Creates a `async_impl::Client` + let default_dns_client = Client::builder() + .dns_resolver(default_dns_resolver) + .build() + .unwrap(); + + let default_dns_response = default_dns_client + .request( + Request::builder() + .url("https://www.example.com") + .body(Body::empty())?, + ) + .await?; + + // Reads the body of `Response` by using `BodyReader`. + let _ = Downloader::console(default_dns_response).download().await; + + Ok(()) +} diff --git a/ylong_http_client/examples/async_http_doh.rs b/ylong_http_client/examples/async_http_doh.rs new file mode 100644 index 0000000..6cddb94 --- /dev/null +++ b/ylong_http_client/examples/async_http_doh.rs @@ -0,0 +1,54 @@ +// Copyright (c) 2025 Huawei Device Co., Ltd. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! This is a simple asynchronous HTTP client example using the +//! ylong_http_client crate. It demonstrates creating a client, making a +//! request, and reading the response asynchronously. + +use ylong_http_client::async_impl::{Body, Client, DohResolver, Downloader, Request}; +use ylong_http_client::HttpClientError; + +fn main() -> Result<(), HttpClientError> { + let handle = ylong_runtime::spawn(async move { + connect().await.unwrap(); + }); + + let _ = ylong_runtime::block_on(handle); + Ok(()) +} + +async fn connect() -> Result<(), HttpClientError> { + // Creates a `DoH Resolver` + let doh_resolver = DohResolver::new("https://1.12.12.12/dns-query") + .add_doh_server("https://120.53.53.53/dns-query"); + + // Creates a `async_impl::Client` + let doh_client = Client::builder() + .dns_resolver(doh_resolver) + .build() + .unwrap(); + + // Sends request and receives a `Response`. + let doh_response = doh_client + .request( + Request::builder() + .url("https://www.example.com") + .body(Body::empty())?, + ) + .await?; + + // Reads the body of `Response` by using `BodyReader`. + let _ = Downloader::console(doh_response).download().await; + + Ok(()) +} diff --git a/ylong_http_client/src/async_impl/connector/mod.rs b/ylong_http_client/src/async_impl/connector/mod.rs index b41727c..7a2c0a8 100644 --- a/ylong_http_client/src/async_impl/connector/mod.rs +++ b/ylong_http_client/src/async_impl/connector/mod.rs @@ -18,6 +18,7 @@ mod stream; use core::future::Future; use std::io::{Error, ErrorKind}; use std::net::SocketAddr; +use std::str::FromStr; use std::sync::Arc; use ylong_http::request::uri::Uri; @@ -111,6 +112,9 @@ async fn dns_query( resolver: Arc, addr: &str, ) -> Result, HttpClientError> { + if let Ok(socket_addr) = SocketAddr::from_str(addr) { + return Ok(vec![socket_addr]); + } let addr_fut = resolver.resolve(addr); let socket_addr = addr_fut.await.map_err(|e| { HttpClientError::from_dns_error( diff --git a/ylong_http_client/src/async_impl/dns/default.rs b/ylong_http_client/src/async_impl/dns/default.rs new file mode 100644 index 0000000..864bf96 --- /dev/null +++ b/ylong_http_client/src/async_impl/dns/default.rs @@ -0,0 +1,254 @@ +// Copyright (c) 2025 Huawei Device Co., Ltd. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io; +use std::net::{SocketAddr, ToSocketAddrs}; +use std::time::{Duration, Instant}; + +use crate::async_impl::dns::resolver::{DefaultDnsFuture, DnsManager, DnsResult, ResolvedAddrs}; +use crate::async_impl::{Resolver, SocketFuture}; + +/// Default dns resolver used by the `Client`. +/// DefaultDnsResolver provides DNS resolver with caching mechanism. +/// +/// # Examples +/// +/// ``` +/// use ylong_http_client::async_impl::{Client, DefaultDnsResolver}; +/// +/// let default_resolver = DefaultDnsResolver::new(); +/// let _client = Client::builder() +/// .dns_resolver(default_resolver) +/// .build() +/// .unwrap(); +/// ``` +pub struct DefaultDnsResolver { + /// Use global if None. + manager: Option, + connector: DefaultDnsConnector, +} + +impl Default for DefaultDnsResolver { + // Default constructor for `DefaultDnsResolver`, with a default TTL of 60 + // seconds. + fn default() -> Self { + DefaultDnsResolver { + manager: Some(DnsManager::default()), + connector: DefaultDnsConnector {}, + } + } +} + +impl DefaultDnsResolver { + /// Creates a new DefaultDnsResolver. + /// + /// # Examples + /// + /// ``` + /// use ylong_http_client::async_impl::DefaultDnsResolver; + /// + /// let res = DefaultDnsResolver::new(); + /// ``` + pub fn new() -> Self { + DefaultDnsResolver { + manager: Some(DnsManager::default()), + connector: DefaultDnsConnector {}, + } + } + + /// Sets whether to use global DNS cache, default is false. + /// + /// # Examples + /// + /// ``` + /// use ylong_http_client::async_impl::DefaultDnsResolver; + /// + /// let res = DefaultDnsResolver::new().global_dns_cache(true); + /// ``` + pub fn global_dns_cache(mut self, use_global: bool) -> Self { + self.manager = (!use_global).then(DnsManager::default); + self + } + + /// Sets DNS ttl, default is 60 second. + /// + /// This will does nothing if `global_dns_cache` is set to true. + /// + /// # Examples + /// + /// ``` + /// use std::time::Duration; + /// + /// use ylong_http_client::async_impl::DefaultDnsResolver; + /// + /// let res = DefaultDnsResolver::new().set_ttl(Duration::from_secs(30)); + /// ``` + pub fn set_ttl(mut self, ttl: Duration) -> Self { + if let Some(manager) = self.manager.as_mut() { + manager.ttl = ttl + } + self + } +} + +#[derive(Clone)] +struct DefaultDnsConnector {} + +impl DefaultDnsConnector { + // Resolves the authority to a list of socket addresses + fn get_socket_addrs(&self, authority: &str) -> Result, io::Error> { + authority + .to_socket_addrs() + .map(|addrs| addrs.collect()) + .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) + } +} + +impl Resolver for DefaultDnsResolver { + fn resolve(&self, authority: &str) -> SocketFuture { + let authority = authority.to_string(); + let (map, ttl) = match &self.manager { + None => { + let manager = DnsManager::global_dns_manager(); + let manager_guard = manager.lock().unwrap(); + manager_guard.clean_expired_entries(); + (manager_guard.map.clone(), manager_guard.ttl) + } + Some(manager) => { + manager.clean_expired_entries(); + (manager.map.clone(), manager.ttl) + } + }; + let connector = self.connector.clone(); + + let handle = crate::runtime::spawn_blocking(move || { + let mut map_lock = map.lock().unwrap(); + if let Some(addrs) = map_lock.get(&authority) { + if addrs.is_valid() { + return Ok(ResolvedAddrs::new(addrs.addr.clone().into_iter())); + } + } + match connector.get_socket_addrs(&authority) { + Ok(addrs) => { + let dns_result = DnsResult::new(addrs.clone(), Instant::now() + ttl); + map_lock.insert(authority, dns_result); + Ok(ResolvedAddrs::new(addrs.into_iter())) + } + Err(err) => Err(io::Error::new(io::ErrorKind::Other, err)), + } + }); + Box::pin(DefaultDnsFuture::new(handle)) + } +} + +#[cfg(test)] +mod ut_dns_test { + use super::*; + + /// UT test case for `DefaultDnsResolver::global_dns_cache()` + /// + /// # Brief + /// 1. Creates a new `DefaultDnsResolver` instance. + /// 2. Verifies the default `manager` is None. + /// 3. Calls `global_dns_cache` and check manager. + #[test] + fn ut_dns_resolver_global() { + let mut resolver = DefaultDnsResolver::new(); + assert!(resolver.manager.is_some()); + resolver = resolver.global_dns_cache(true); + assert!(resolver.manager.is_none()); + resolver = resolver.global_dns_cache(false); + assert!(resolver.manager.is_some()); + } + + /// UT test case for `DefaultDnsResolver::set_ttl()` + /// + /// # Brief + /// 1. Creates a new `DefaultDnsResolver` instance. + /// 2. Verifies the default `ttl` is 60 second. + /// 3. Calls `set_ttl` and check ttl. + #[test] + fn ut_dns_resolver_ttl() { + let mut resolver = DefaultDnsResolver::new(); + assert!(resolver.manager.is_some()); + assert_eq!( + resolver.manager.as_ref().unwrap().ttl, + Duration::from_secs(60) + ); + resolver = resolver.set_ttl(Duration::from_secs(30)); + assert_eq!( + resolver.manager.as_ref().unwrap().ttl, + Duration::from_secs(30) + ); + } + + /// UT test case for `DefaultDnsResolver::resolve` + /// + /// # Brief + /// 1. Creates a default dns resolver with 50ms ttl. + /// 2. Calls resolve to get socket address twice. + /// 3. Verifies the second resolver is faster than the first one. + /// 4. Verifies the second resolver result as same as the first one. + #[tokio::test] + #[cfg(feature = "tokio_base")] + async fn ut_defualt_dns_resolver_resolve() { + let authority = "example.com:0"; + let resolver = DefaultDnsResolver::new().set_ttl(Duration::from_secs(50)); + let start1 = Instant::now(); + let addrs1 = resolver.resolve(authority).await; + let duration1 = start1.elapsed(); + assert!(addrs1.is_ok()); + tokio::time::sleep(Duration::from_millis(10)).await; + let start2 = Instant::now(); + let addrs2 = resolver.resolve(authority).await; + let duration2 = start2.elapsed(); + assert!(duration1 > duration2); + assert!(addrs2.is_ok()); + if let (Ok(addr1), Ok(addr2)) = (addrs1, addrs2) { + let vec1: Vec = addr1.collect(); + let vec2: Vec = addr2.collect(); + assert_eq!(vec1, vec2); + } + } + + /// UT test case for `DefaultDnsResolver::resolve` + /// + /// # Brief + /// 1. Creates a default dns resolver with 50ms ttl. + /// 2. Calls resolve to get socket address twice. + /// 3. Verifies the second resolver is faster than the first one. + /// 4. Verifies the second resolver result as same as the first one. + #[test] + #[cfg(feature = "ylong_base")] + fn ut_dns_resolver_resolve() { + ylong_runtime::block_on(async { + let authority = "example.com:0"; + let resolver = DefaultDnsResolver::new().set_ttl(Duration::from_secs(50)); + let start1 = Instant::now(); + let addrs1 = resolver.resolve(authority).await; + let duration1 = start1.elapsed(); + assert!(addrs1.is_ok()); + ylong_runtime::time::sleep(Duration::from_millis(10)).await; + let start2 = Instant::now(); + let addrs2 = resolver.resolve(authority).await; + let duration2 = start2.elapsed(); + assert!(duration1 > duration2); + assert!(addrs2.is_ok()); + if let (Ok(addr1), Ok(addr2)) = (addrs1, addrs2) { + let vec1: Vec = addr1.collect(); + let vec2: Vec = addr2.collect(); + assert_eq!(vec1, vec2); + } + }); + } +} diff --git a/ylong_http_client/src/async_impl/dns/doh.rs b/ylong_http_client/src/async_impl/dns/doh.rs new file mode 100644 index 0000000..a9136da --- /dev/null +++ b/ylong_http_client/src/async_impl/dns/doh.rs @@ -0,0 +1,435 @@ +// Copyright (c) 2025 Huawei Device Co., Ltd. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::str::FromStr; +use std::time::{Duration, Instant}; + +use crate::async_impl::dns::resolver::{DefaultDnsFuture, DnsManager, DnsResult, ResolvedAddrs}; +use crate::async_impl::{Body, Client, Request, Resolver, SocketFuture}; +use crate::HttpClientError; + +const DEFAULT_MAX_RETRY_COUNT: i32 = 1; + +/// Doh resolver used by the `Client`. +/// +/// # Examples +/// +/// ``` +/// use ylong_http_client::async_impl::{Client, DohResolver}; +/// +/// let doh_resolver = DohResolver::new("https://1.12.12.12/dns-query"); +/// let _doh_client = Client::builder() +/// .dns_resolver(doh_resolver) +/// .build() +/// .unwrap(); +/// ``` +pub struct DohResolver { + manager: Option, + connector: DohConnector, +} + +impl DohResolver { + /// Creates a new DohResolver. And sets DOH server. + /// + /// # Examples + /// + /// ``` + /// use ylong_http_client::async_impl::DohResolver; + /// + /// let res = DohResolver::new("https://1.12.12.12/dns-query"); + /// ``` + pub fn new(doh_server: &str) -> Self { + Self { + manager: Some(DnsManager::default()), + connector: DohConnector::new(doh_server), + } + } + + /// Adds the doh server. + /// + /// # Examples + /// + /// ``` + /// use ylong_http_client::async_impl::DohResolver; + /// + /// let res = DohResolver::new("https://1.12.12.12/dns-query") + /// .add_doh_server("https://1.12.12.12/dns-query"); + /// ``` + pub fn add_doh_server(mut self, doh_server: &str) -> Self { + self.connector.add_doh_server(doh_server); + self + } + + /// Sets whether to use global DNS cache, default is false. + /// + /// # Examples + /// + /// ``` + /// use ylong_http_client::async_impl::DohResolver; + /// + /// let res = DohResolver::new("https://1.12.12.12/dns-query").global_dns_cache(false); + /// ``` + pub fn global_dns_cache(mut self, use_global: bool) -> Self { + self.manager = (!use_global).then(DnsManager::default); + self + } + + /// Sets DNS ttl, default is 60 second. + /// + /// This will does nothing if `global_dns_cache` is set to true. + /// + /// # Examples + /// + /// ``` + /// use std::time::Duration; + /// + /// use ylong_http_client::async_impl::DohResolver; + /// + /// let res = DohResolver::new("https://1.12.12.12/dns-query").set_ttl(Duration::from_secs(30)); + /// ``` + pub fn set_ttl(mut self, ttl: Duration) -> Self { + if let Some(manager) = self.manager.as_mut() { + manager.ttl = ttl + } + self + } +} + +#[derive(Clone)] +struct DohConnector { + doh_servers: Vec, + max_retry_count: i32, +} + +impl DohConnector { + fn new(doh_server: &str) -> Self { + DohConnector { + doh_servers: vec![doh_server.to_string()], + max_retry_count: DEFAULT_MAX_RETRY_COUNT, + } + } + + fn add_doh_server(&mut self, doh_server: &str) { + self.doh_servers.push(doh_server.to_string()); + } + + async fn retry(&self, authority: &str) -> Result<(Vec, u64), HttpClientError> { + for _ in 0..self.max_retry_count { + for server in self.doh_servers.iter() { + if let Ok((socket_addr, ttl)) = self.doh_connect(authority, server.clone()).await { + return Ok((socket_addr, ttl)); + } + } + } + Err(HttpClientError::from_str( + crate::ErrorKind::Connect, + "Can't find valid address", + )) + } + + /// Connects to the DOH server and retrieves DNS information. + async fn doh_connect( + &self, + authority: &str, + doh_server: String, + ) -> Result<(Vec, u64), HttpClientError> { + let part: Vec<&str> = authority.split(':').collect(); + let host: &str = part[0]; + let port: u16 = part[1].parse().unwrap(); + let url_4 = format!("{}?name={}&type=A", doh_server, host); + let url_6 = format!("{}?name={}&type=AAAA", doh_server, host); + let client_4 = Client::builder().build()?; + let client_6 = Client::builder().build()?; + let request_4 = Request::builder().url(&url_4).body(Body::empty())?; + let request_6 = Request::builder().url(&url_6).body(Body::empty())?; + let response_4 = client_4.request(request_4).await?; + let response_6 = client_6.request(request_6).await?; + let text_4 = response_4.text().await?; + let text_6 = response_6.text().await?; + let text = format!("{},{}", text_4, text_6); + Ok(Self::get_info(&text, port)) + } + + /// Parses and extracts information from the DNS response text. + fn get_info(text: &str, port: u16) -> (Vec, u64) { + let mut ips = Vec::new(); + let mut start = 0; + let mut ttl = u64::MAX; + while let Some((answer_end, answer_str)) = Self::get_answer_str(text, start) { + if let Some(socket_addr) = Self::get_socket_addr(answer_str, port) { + if let Some(answer_ttl) = Self::get_ttl(answer_str) { + ips.push(socket_addr); + ttl = std::cmp::min(ttl, answer_ttl); + } + } + start = answer_end + 1; + } + ttl = if ttl == u64::MAX { 0 } else { ttl }; + (ips, ttl) + } + + fn get_answer_str(answer_section: &str, start: usize) -> Option<(usize, &str)> { + let answer_start = answer_section[start..].find('{').map(|pos| start + pos)?; + let answer_end = answer_section[answer_start..].find('}').unwrap() + answer_start; + Some((answer_end, &answer_section[answer_start..answer_end])) + } + + fn get_socket_addr(answer_str: &str, port: u16) -> Option { + let data_str = r#""data":""#; + if let Some(ip_pos) = answer_str.find(data_str) { + let ip_start = ip_pos + data_str.len(); + if let Some(ip_end) = answer_str[ip_start..].find('\"') { + let ip = &answer_str[ip_start..ip_start + ip_end]; + if let Ok(ipv4_addr) = Ipv4Addr::from_str(ip) { + return Some(SocketAddr::new(IpAddr::V4(ipv4_addr), port)); + } + if let Ok(ipv6_addr) = Ipv6Addr::from_str(ip) { + return Some(SocketAddr::new(IpAddr::V6(ipv6_addr), port)); + } + } + } + None + } + + fn get_ttl(answer_str: &str) -> Option { + let ttl_str = r#""TTL":"#; + if let Some(ttl_pos) = answer_str.find(ttl_str) { + let ttl_start = ttl_pos + ttl_str.len(); + if let Some(ttl_end) = answer_str[ttl_start..].find(',') { + let ttl: u64 = answer_str[ttl_start..ttl_start + ttl_end].parse().unwrap(); + return Some(ttl); + } + } + None + } +} + +impl Resolver for DohResolver { + fn resolve(&self, authority: &str) -> SocketFuture { + let authority = authority.to_string(); + let map = match &self.manager { + None => { + let manager = DnsManager::global_dns_manager(); + let manager_guard = manager.lock().unwrap(); + manager_guard.clean_expired_entries(); + manager_guard.map.clone() + } + Some(manager) => { + manager.clean_expired_entries(); + manager.map.clone() + } + }; + let connector = self.connector.clone(); + let handle = crate::runtime::spawn_blocking(move || { + let mut map_lock = map.lock().unwrap(); + if let Some(addrs) = map_lock.get(&authority) { + if addrs.is_valid() { + return Ok(ResolvedAddrs::new(addrs.addr.clone().into_iter())); + } + } + #[cfg(feature = "ylong_base")] + let result = ylong_runtime::block_on(connector.retry(&authority)); + #[cfg(feature = "tokio_base")] + let result = tokio::runtime::Runtime::new() + .unwrap() + .block_on(connector.retry(&authority)); + match result { + Ok((addrs, ttl)) => { + let dns_result = + DnsResult::new(addrs.clone(), Instant::now() + Duration::from_secs(ttl)); + map_lock.insert(authority, dns_result); + Ok(ResolvedAddrs::new(addrs.into_iter())) + } + Err(err) => Err(io::Error::new(io::ErrorKind::Other, err)), + } + }); + Box::pin(DefaultDnsFuture::new(handle)) + } +} + +#[cfg(test)] +mod ut_doh_test { + use super::*; + + /// UT test case for `DohResolver::global_dns_cache` + /// + /// # Brief + /// 1. Creates a new `DohResolver` instance. + /// 2. Verifies the default `manager` is None. + /// 3. Calls `global_dns_cache` and check manager. + #[test] + fn ut_dns_resolver_global() { + let mut resolver = DohResolver::new("https://1.12.12.12/dns-query"); + assert!(resolver.manager.is_some()); + resolver = resolver.global_dns_cache(true); + assert!(resolver.manager.is_none()); + resolver = resolver.global_dns_cache(false); + assert!(resolver.manager.is_some()); + } + + /// UT test case for `DohResolver::set_ttl()` + /// + /// # Brief + /// 1. Creates a new `DohResolver` instance. + /// 2. Verifies the default `ttl` is 60 second. + /// 3. Calls `set_ttl` and check ttl. + #[test] + fn ut_dns_resolver_ttl() { + let mut resolver = DohResolver::new("https://1.12.12.12/dns-query"); + assert!(resolver.manager.is_some()); + assert_eq!( + resolver.manager.as_ref().unwrap().ttl, + Duration::from_secs(60) + ); + resolver = resolver.set_ttl(Duration::from_secs(30)); + assert_eq!( + resolver.manager.as_ref().unwrap().ttl, + Duration::from_secs(30) + ); + } + + /// UT test case for `get_info` function with IPv4 address + /// + /// # Brief + /// 1. Provides a DNS response text for an IPv4 address. + /// 2. Calls `get_info` to extract addresses and TTL. + /// 3. Verifies the extracted address and TTL. + #[test] + fn ut_get_info_ipv4() { + let ipv4_text = r#"{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"example.com.","type":1}],"Answer":[{"name":"example.com.","type":1,"TTL":3378,"data":"93.184.215.14"}]}"#; + let (addrs, ttl) = DohConnector::get_info(ipv4_text, 0); + assert_eq!(addrs, vec![SocketAddr::from(([93, 184, 215, 14], 0))]); + assert_eq!(ttl, 3378); + } + + /// UT test case for `get_info` function with IPv6 address + /// + /// # Brief + /// 1. Provides a DNS response text for an IPv6 address. + /// 2. Calls `get_info` to extract addresses and TTL. + /// 3. Verifies the extracted address and TTL. + #[test] + fn ut_get_info_ipv6() { + let ipv6_text = r#"{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"example.com.","type":28}]"Answer":[{"name":example.com.","type":28,"TTL":1466,"data":"2606:2800:21f:cb07:6820:80da:af6b:8b2c"}]}"#; + let (addrs, ttl) = DohConnector::get_info(ipv6_text, 0); + assert_eq!( + addrs, + vec![SocketAddr::from(( + [0x2606, 0x2800, 0x21f, 0xcb07, 0x6820, 0x80da, 0xaf6b, 0x8b2c], + 0 + ))] + ); + assert_eq!(ttl, 1466); + } + + /// UT test case for `get_info` function with both IPv4 and IPv6 addresses + /// + /// # Brief + /// 1. Provides a DNS response text with both IPv4 and IPv6 addresses. + /// 2. Calls `get_info` to extract the addresses and TTL. + /// 3. Verifies the extracted addresses and TTL. + #[test] + fn ut_get_info_both() { + let text = r#"{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"example.com.","type":1}],"Answer":[{"name":"example.com.","type":1,"TTL":3378,"data":"93.184.215.14"}]},{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"example.com.","type":28}],"Answer":[{"name":"example.com.","type":28,"TTL":1466,"data":"2606:2800:21f:cb07:6820:80da:af6b:8b2c"}]}"#; + let (addrs, ttl) = DohConnector::get_info(text, 0); + assert_eq!( + addrs, + vec![ + SocketAddr::from(([93, 184, 215, 14], 0)), + SocketAddr::from(( + [0x2606, 0x2800, 0x21f, 0xcb07, 0x6820, 0x80da, 0xaf6b, 0x8b2c], + 0 + )), + ] + ); + assert_eq!(ttl, 1466); + } + + /// UT test case for `get_info` function with some error response. + /// + /// # Brief + /// 1. Provides a DNS response text with some error response. + /// 2. Calls `get_info` to extract the addresses and TTL. + /// 3. Verifies addresses is empty and TTL is the max of u64. + #[test] + fn ut_get_info_error() { + let error_text = "This is some error response."; + let (addrs, ttl) = DohConnector::get_info(error_text, 0); + assert_eq!(addrs, vec![]); + assert_eq!(ttl, 0); + } + + /// UT test case for `DohResolver::resolve` + /// + /// # Brief + /// 1. Creates a doh resolver. + /// 2. Calls resolve to get socket address twice. + /// 3. Verifies the second resolver is faster than the first one. + /// 4. Verifies the second resolver result as same as the first one. + #[tokio::test] + #[cfg(feature = "tokio_base")] + async fn ut_doh_resolver_resolve() { + let authority = "example.com:0"; + let resolver = + DohResolver::new("https://1.12.12.12/dns-query").set_ttl(Duration::from_secs(50)); + let start1 = Instant::now(); + let addrs1 = resolver.resolve(authority).await; + let duration1 = start1.elapsed(); + assert!(addrs1.is_ok()); + tokio::time::sleep(Duration::from_millis(10)).await; + let start2 = Instant::now(); + let addrs2 = resolver.resolve(authority).await; + let duration2 = start2.elapsed(); + assert!(duration1 > duration2); + assert!(addrs2.is_ok()); + if let (Ok(addr1), Ok(addr2)) = (addrs1, addrs2) { + let vec1: Vec = addr1.collect(); + let vec2: Vec = addr2.collect(); + assert_eq!(vec1, vec2); + } + } + + /// UT test case for `DohResolver::resolve` + /// + /// # Brief + /// 1. Creates a doh resolver. + /// 2. Calls resolve to get socket address twice. + /// 3. Verifies the second resolver is faster than the first one. + /// 4. Verifies the second resolver result as same as the first one. + #[test] + #[cfg(feature = "ylong_base")] + fn ut_doh_resolver_resolve() { + ylong_runtime::block_on(async { + let authority = "example.com:0"; + let resolver = + DohResolver::new("https://1.12.12.12/dns-query").set_ttl(Duration::from_secs(50)); + let start1 = Instant::now(); + let addrs1 = resolver.resolve(authority).await; + let duration1 = start1.elapsed(); + assert!(addrs1.is_ok()); + ylong_runtime::time::sleep(Duration::from_millis(10)).await; + let start2 = Instant::now(); + let addrs2 = resolver.resolve(authority).await; + let duration2 = start2.elapsed(); + assert!(duration1 > duration2); + assert!(addrs2.is_ok()); + if let (Ok(addr1), Ok(addr2)) = (addrs1, addrs2) { + let vec1: Vec = addr1.collect(); + let vec2: Vec = addr2.collect(); + assert_eq!(vec1, vec2); + } + }); + } +} diff --git a/ylong_http_client/src/async_impl/dns/mod.rs b/ylong_http_client/src/async_impl/dns/mod.rs index 2b8091d..b494696 100644 --- a/ylong_http_client/src/async_impl/dns/mod.rs +++ b/ylong_http_client/src/async_impl/dns/mod.rs @@ -21,8 +21,14 @@ //! //! - [`DefaultDnsResolver`]: Default dns resolver. +mod default; +#[cfg(feature = "__c_openssl")] +mod doh; mod happy_eyeballs; mod resolver; +pub use default::DefaultDnsResolver; +#[cfg(feature = "__c_openssl")] +pub use doh::DohResolver; pub(crate) use happy_eyeballs::{EyeBallConfig, HappyEyeballs}; -pub use resolver::{Addrs, DefaultDnsResolver, Resolver, SocketFuture, StdError}; +pub use resolver::{Addrs, Resolver, SocketFuture, StdError}; diff --git a/ylong_http_client/src/async_impl/dns/resolver.rs b/ylong_http_client/src/async_impl/dns/resolver.rs index d78fd89..56be928 100644 --- a/ylong_http_client/src/async_impl/dns/resolver.rs +++ b/ylong_http_client/src/async_impl/dns/resolver.rs @@ -16,18 +16,17 @@ use std::collections::HashMap; use std::future::Future; use std::io; -use std::io::Error; -use std::net::{SocketAddr, ToSocketAddrs}; +use std::net::SocketAddr; use std::pin::Pin; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, OnceLock}; use std::task::{Context, Poll}; use std::time::{Duration, Instant}; use std::vec::IntoIter; use crate::runtime::JoinHandle; +const DEFAULT_MAX_LEN: usize = 30000; const DEFAULT_TTL: Duration = Duration::from_secs(60); -const MAX_ENTRIES_LEN: usize = 30000; /// `SocketAddr` resolved by `Resolver`. pub type Addrs = Box + Sync + Send>; @@ -79,7 +78,13 @@ impl Iterator for ResolvedAddrs { /// Futures generated by `DefaultDnsResolver`. pub struct DefaultDnsFuture { - inner: JoinHandle>, + inner: JoinHandle>, +} + +impl DefaultDnsFuture { + pub(crate) fn new(handle: JoinHandle>) -> Self { + DefaultDnsFuture { inner: handle } + } } impl Future for DefaultDnsFuture { @@ -89,209 +94,136 @@ impl Future for DefaultDnsFuture { Pin::new(&mut self.inner).poll(cx).map(|res| match res { Ok(Ok(addrs)) => Ok(Box::new(addrs) as Addrs), Ok(Err(err)) => Err(Box::new(err) as StdError), - Err(err) => Err(Box::new(Error::new(io::ErrorKind::Interrupted, err)) as StdError), + Err(err) => Err(Box::new(io::Error::new(io::ErrorKind::Interrupted, err)) as StdError), }) } } -/// Default dns resolver used by the `Client`. -/// DefaultDnsResolver provides DNS resolver with caching machanism. -pub struct DefaultDnsResolver { - manager: DnsManager, // Manages DNS cache - connector: DnsConnector, // Performing DNS resolution - ttl: Duration, // Time-to-live for the DNS cache +pub(crate) struct DnsManager { + /// Cache storing authority and DNS results + pub(crate) map: Arc>>, + max_entries_len: usize, + /// Time-to-live for the DNS cache + pub(crate) ttl: Duration, } -impl Default for DefaultDnsResolver { - // Default constructor for `DefaultDnsResolver`, with a default TTL of 60 +impl Default for DnsManager { + // Default constructor for `DnsManager`, with a default TTL of 60 // seconds. fn default() -> Self { - DefaultDnsResolver { - manager: DnsManager::default(), - connector: DnsConnector {}, + DnsManager { + map: Default::default(), + max_entries_len: DEFAULT_MAX_LEN, ttl: DEFAULT_TTL, // Default TTL set to 60 seconds } } } -impl DefaultDnsResolver { - /// Create a new DefaultDnsResolver. And TTL is Time to live for cache. - /// - /// # Examples - /// - /// ``` - /// use std::time::Duration; - /// - /// use ylong_http_client::async_impl::DefaultDnsResolver; - /// - /// let res = DefaultDnsResolver::new(Duration::from_secs(1)); - /// ``` - pub fn new(ttl: Duration) -> Self { - DefaultDnsResolver { - manager: DnsManager::new(), - connector: DnsConnector {}, - ttl, // Set TTL through the passed parameters - } - } -} - -#[derive(Default)] -struct DnsManager { - // Cache storing authority and DNS results - map: Mutex>, -} - impl DnsManager { - // Creates a new `DnsManager` instance with an empty cache - fn new() -> Self { - DnsManager { - map: Mutex::new(HashMap::new()), - } + /// Global DNS Manager + pub(crate) fn global_dns_manager() -> Arc> { + static GLOBAL_DNS_MANAGER: OnceLock>> = OnceLock::new(); + GLOBAL_DNS_MANAGER + .get_or_init(|| Arc::new(Mutex::new(DnsManager::default()))) + .clone() } - // Cleans expired DNS cache entries by retaining only valid ones - fn clean_expired_entries(&self) { + /// Cleans expired DNS cache entries by retaining only valid ones + pub(crate) fn clean_expired_entries(&self) { let mut map_lock = self.map.lock().unwrap(); - if map_lock.len() > MAX_ENTRIES_LEN { - map_lock.retain(|_, result| result.inner.lock().unwrap().is_valid()); + if map_lock.len() > self.max_entries_len { + map_lock.retain(|_, result| result.is_valid()); } } } -struct DnsResult { - inner: Arc>, +#[derive(Clone)] +pub(crate) struct DnsResult { + /// List of resolved addresses for the authority + pub(crate) addr: Vec, + /// Expiration time for the cache entry + expiration_time: Instant, } impl DnsResult { - // Creates a new DNS result with the given addresses and expiration time - fn new(addr: Vec, expiration_time: Instant) -> Self { + /// Creates a new DNS result with the given addresses and expiration time + pub(crate) fn new(addr: Vec, expiration_time: Instant) -> Self { DnsResult { - inner: Arc::new(Mutex::new(DnsResultInner { - addr, - expiration_time, - })), + addr, + expiration_time, } } -} -#[derive(Clone)] -struct DnsResultInner { - addr: Vec, // List of resolved addresses for the authority - expiration_time: Instant, // Expiration time for the cache entry -} - -impl DnsResultInner { - // Checks if the DNS result is still valid - fn is_valid(&self) -> bool { + /// Checks if the DNS result is still valid + pub(crate) fn is_valid(&self) -> bool { self.expiration_time > Instant::now() } } -impl Default for DnsResultInner { - // Default constructor for `DnsResultInner`, with an empty address list and 60 +impl Default for DnsResult { + // Default constructor for `DnsResult`, with an empty address list and 60 // seconds expiration fn default() -> Self { - DnsResultInner { + DnsResult { addr: vec![], - expiration_time: Instant::now() + Duration::from_secs(60), + expiration_time: Instant::now() + DEFAULT_TTL, } } } -struct DnsConnector {} - -impl DnsConnector { - // Resolves the authority to a list of socket addresses - fn get_socket_addrs(&self, authority: &str) -> Result, io::Error> { - authority - .to_socket_addrs() - .map(|addrs| addrs.collect()) - .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) - } -} - -impl Resolver for DefaultDnsResolver { - fn resolve(&self, authority: &str) -> SocketFuture { - let authority = authority.to_string(); - self.manager.clean_expired_entries(); - Box::pin(async move { - let mut map_lock = self.manager.map.lock().unwrap(); - if let Some(addrs) = map_lock.get(&authority) { - let lock_inner = addrs.inner.lock().unwrap(); - if lock_inner.is_valid() { - return Ok(Box::new(lock_inner.addr.clone().into_iter()) as Addrs); - } - } - match self.connector.get_socket_addrs(&authority) { - Ok(addrs) => { - let dns_result = DnsResult::new(addrs.clone(), Instant::now() + self.ttl); - map_lock.insert(authority, dns_result); - Ok(Box::new(addrs.into_iter()) as Addrs) - } - Err(err) => Err(Box::new(err) as StdError), - } - }) - } -} - -#[cfg(feature = "tokio_base")] #[cfg(test)] -mod ut_dns_cache { +mod ut_resover_test { use super::*; - /// UT test cases for `DefaultDnsResolver::resolve`. + /// UT test case for `DnsManager::new` /// /// # Brief - /// 1. Verify the first DNS result is cached when connected to Internet or - /// return error when without Internet. - /// 2. Verify the second DNS result as same as the first one. - #[tokio::test] - async fn ut_default_dns_resolver() { - let domain = "example.com:0"; - let resolver = DefaultDnsResolver::new(std::time::Duration::from_millis(100)); - let result1 = resolver.resolve(domain).await; - let result2 = resolver.resolve(domain).await; - let result1 = result1 - .map(|a| a.collect::>()) - .err() - .map(|e| e.to_string()); - let result2 = result2 - .map(|a| a.collect::>()) - .err() - .map(|e| e.to_string()); - assert_eq!(result1, result2); - } -} - -#[cfg(feature = "ylong_base")] -#[cfg(test)] -mod ut_dns_cache { - use super::*; - - /// UT test cases for `DefaultDnsResolver::resolve`. + /// 1. Creates a new `DnsManager` instance. + /// 2. Verifies the default `max_entries_len` is 30000. + /// 3. Sets and verifies a new `max_entries_len` of 1. + #[test] + fn ut_dns_manager_new() { + let manager = DnsManager::default(); + assert_eq!(manager.max_entries_len, 30000); + let mut map = manager.map.lock().unwrap(); + map.insert( + "example.com".to_string(), + DnsResult::new(vec![SocketAddr::from(([0, 0, 0, 1], 1))], Instant::now()), + ); + assert!(map.contains_key("example.com")); + } + + /// UT test case for `DnsManager::clean_expired_entries` /// /// # Brief - /// 1. Verify the first DNS result is cached when connected to Internet or - /// return error when without Internet. - /// 2. Verify the second DNS result as same as the first one. + /// 1. Creates a `DnsManager` instance and sets `max_entries_len` to 1. + /// 2. Adds two DNS results to the cache: one valid and one expired. + /// 3. Calls `clean_expired_entries` to remove expired entries. + /// 4. Verifies the expired entry is removed from the cache. #[test] - fn ut_default_dns_resolver() { - ylong_runtime::block_on(ut_default_dns_resolver_async()); - } - - async fn ut_default_dns_resolver_async() { - let domain = "example.com:0"; - let resolver = DefaultDnsResolver::new(std::time::Duration::from_millis(100)); - let result1 = resolver.resolve(domain).await; - let result2 = resolver.resolve(domain).await; - let result1 = result1 - .map(|a| a.collect::>()) - .err() - .map(|e| e.to_string()); - let result2 = result2 - .map(|a| a.collect::>()) - .err() - .map(|e| e.to_string()); - assert_eq!(result1, result2); + fn ut_dns_manager_clean_cache() { + let manager = DnsManager { + max_entries_len: 1, + ..Default::default() + }; + let mut map = manager.map.lock().unwrap(); + map.insert( + "example1.com".to_string(), + DnsResult::new( + vec![SocketAddr::from(([0, 0, 0, 1], 1))], + Instant::now() + Duration::from_secs(60), + ), + ); + map.insert( + "example2.com".to_string(), + DnsResult::new( + vec![SocketAddr::from(([0, 0, 0, 2], 2))], + Instant::now() - Duration::from_secs(60), + ), + ); + drop(map); + manager.clean_expired_entries(); + assert!(manager.map.lock().unwrap().contains_key("example1.com")); + assert!(!manager.map.lock().unwrap().contains_key("example2.com")); } } diff --git a/ylong_http_client/src/async_impl/mod.rs b/ylong_http_client/src/async_impl/mod.rs index 572a636..fb22002 100644 --- a/ylong_http_client/src/async_impl/mod.rs +++ b/ylong_http_client/src/async_impl/mod.rs @@ -60,4 +60,6 @@ pub use ylong_http::body::{MultiPart, Part}; /// Client Adapter. pub type Client = client::Client; +#[cfg(feature = "__c_openssl")] +pub use dns::DohResolver; pub use dns::{Addrs, DefaultDnsResolver, Resolver, SocketFuture, StdError}; diff --git a/ylong_http_client/src/lib.rs b/ylong_http_client/src/lib.rs index f4bbed2..d1ec7b3 100644 --- a/ylong_http_client/src/lib.rs +++ b/ylong_http_client/src/lib.rs @@ -85,13 +85,14 @@ pub(crate) mod runtime { io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf}, net::TcpStream, sync::{OwnedSemaphorePermit as SemaphorePermit, Semaphore}, - task::JoinHandle, + task::{spawn_blocking, JoinHandle}, time::{sleep, timeout, Sleep}, }; #[cfg(feature = "ylong_base")] pub(crate) use ylong_runtime::{ io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf}, net::TcpStream, + spawn_blocking, sync::Semaphore, task::JoinHandle, time::{sleep, timeout, Sleep}, -- Gitee