icann_rdap_client/http/
wrapped.rs

1//! Wrapped Client.
2
3use icann_rdap_common::httpdata::HttpData;
4pub use reqwest::header::HeaderValue;
5use reqwest::header::{
6    ACCESS_CONTROL_ALLOW_ORIGIN, CACHE_CONTROL, CONTENT_TYPE, EXPIRES, LOCATION, RETRY_AFTER,
7    STRICT_TRANSPORT_SECURITY,
8};
9pub use reqwest::Client as ReqwestClient;
10pub use reqwest::Error as ReqwestError;
11
12use super::create_reqwest_client;
13use super::ReqwestClientConfig;
14use crate::RdapClientError;
15
16#[cfg(not(target_arch = "wasm32"))]
17use {
18    super::create_reqwest_client_with_addr, chrono::DateTime, chrono::Utc, reqwest::StatusCode,
19    std::net::SocketAddr, tracing::debug, tracing::info,
20};
21
22/// Used by the request functions.
23#[derive(Clone, Copy)]
24pub struct RequestOptions {
25    pub(crate) max_retry_secs: u32,
26    pub(crate) def_retry_secs: u32,
27    pub(crate) max_retries: u16,
28}
29
30impl Default for RequestOptions {
31    fn default() -> Self {
32        Self {
33            max_retry_secs: 120,
34            def_retry_secs: 60,
35            max_retries: 1,
36        }
37    }
38}
39
40/// Configures the HTTP client.
41#[derive(Default)]
42pub struct ClientConfig {
43    /// Config for the Reqwest client.
44    client_config: ReqwestClientConfig,
45
46    /// Request options.
47    request_options: RequestOptions,
48}
49
50#[buildstructor::buildstructor]
51impl ClientConfig {
52    #[builder]
53    #[allow(clippy::too_many_arguments)]
54    pub fn new(
55        user_agent_suffix: Option<String>,
56        https_only: Option<bool>,
57        accept_invalid_host_names: Option<bool>,
58        accept_invalid_certificates: Option<bool>,
59        follow_redirects: Option<bool>,
60        host: Option<HeaderValue>,
61        origin: Option<HeaderValue>,
62        timeout_secs: Option<u64>,
63        max_retry_secs: Option<u32>,
64        def_retry_secs: Option<u32>,
65        max_retries: Option<u16>,
66    ) -> Self {
67        let default_cc = ReqwestClientConfig::default();
68        let default_ro = RequestOptions::default();
69        Self {
70            client_config: ReqwestClientConfig {
71                user_agent_suffix: user_agent_suffix.unwrap_or(default_cc.user_agent_suffix),
72                https_only: https_only.unwrap_or(default_cc.https_only),
73                accept_invalid_host_names: accept_invalid_host_names
74                    .unwrap_or(default_cc.accept_invalid_host_names),
75                accept_invalid_certificates: accept_invalid_certificates
76                    .unwrap_or(default_cc.accept_invalid_certificates),
77                follow_redirects: follow_redirects.unwrap_or(default_cc.follow_redirects),
78                host,
79                origin,
80                timeout_secs: timeout_secs.unwrap_or(default_cc.timeout_secs),
81            },
82            request_options: RequestOptions {
83                max_retry_secs: max_retry_secs.unwrap_or(default_ro.max_retry_secs),
84                def_retry_secs: def_retry_secs.unwrap_or(default_ro.def_retry_secs),
85                max_retries: max_retries.unwrap_or(default_ro.max_retries),
86            },
87        }
88    }
89
90    #[builder(entry = "from_config", exit = "build")]
91    #[allow(clippy::too_many_arguments)]
92    pub fn new_from_config(
93        &self,
94        user_agent_suffix: Option<String>,
95        https_only: Option<bool>,
96        accept_invalid_host_names: Option<bool>,
97        accept_invalid_certificates: Option<bool>,
98        follow_redirects: Option<bool>,
99        host: Option<HeaderValue>,
100        origin: Option<HeaderValue>,
101        timeout_secs: Option<u64>,
102        max_retry_secs: Option<u32>,
103        def_retry_secs: Option<u32>,
104        max_retries: Option<u16>,
105    ) -> Self {
106        Self {
107            client_config: ReqwestClientConfig {
108                user_agent_suffix: user_agent_suffix
109                    .unwrap_or(self.client_config.user_agent_suffix.clone()),
110                https_only: https_only.unwrap_or(self.client_config.https_only),
111                accept_invalid_host_names: accept_invalid_host_names
112                    .unwrap_or(self.client_config.accept_invalid_host_names),
113                accept_invalid_certificates: accept_invalid_certificates
114                    .unwrap_or(self.client_config.accept_invalid_certificates),
115                follow_redirects: follow_redirects.unwrap_or(self.client_config.follow_redirects),
116                host: host.map_or(self.client_config.host.clone(), Some),
117                origin: origin.map_or(self.client_config.origin.clone(), Some),
118                timeout_secs: timeout_secs.unwrap_or(self.client_config.timeout_secs),
119            },
120            request_options: RequestOptions {
121                max_retry_secs: max_retry_secs.unwrap_or(self.request_options.max_retry_secs),
122                def_retry_secs: def_retry_secs.unwrap_or(self.request_options.def_retry_secs),
123                max_retries: max_retries.unwrap_or(self.request_options.max_retries),
124            },
125        }
126    }
127}
128
129/// A wrapper around Reqwest client to give additional features when used with the request functions.
130pub struct Client {
131    /// The reqwest client.
132    pub(crate) reqwest_client: ReqwestClient,
133
134    /// Request options.
135    pub(crate) request_options: RequestOptions,
136}
137
138impl Client {
139    pub fn new(reqwest_client: ReqwestClient, request_options: RequestOptions) -> Self {
140        Self {
141            reqwest_client,
142            request_options,
143        }
144    }
145}
146
147/// Creates a wrapped HTTP client. The wrapped
148/// client holds its own connection pools, so in many
149/// uses cases creating only one client per process is
150/// necessary.
151pub fn create_client(config: &ClientConfig) -> Result<Client, RdapClientError> {
152    let client = create_reqwest_client(&config.client_config)?;
153    Ok(Client::new(client, config.request_options))
154}
155
156/// Creates a wrapped HTTP client.
157/// This will direct the underlying client to connect to a specific socket.
158#[cfg(not(target_arch = "wasm32"))]
159pub fn create_client_with_addr(
160    config: &ClientConfig,
161    domain: &str,
162    addr: SocketAddr,
163) -> Result<Client, RdapClientError> {
164    let client = create_reqwest_client_with_addr(&config.client_config, domain, addr)?;
165    Ok(Client::new(client, config.request_options))
166}
167
168pub(crate) struct WrappedResponse {
169    pub(crate) http_data: HttpData,
170    pub(crate) text: String,
171}
172
173pub(crate) async fn wrapped_request(
174    url: &str,
175    client: &Client,
176) -> Result<WrappedResponse, ReqwestError> {
177    // send request and loop for possible retries
178    #[allow(unused_mut)] //because of wasm32 exclusion below
179    let mut response = client.reqwest_client.get(url).send().await?;
180
181    // this doesn't work on wasm32 because tokio doesn't work on wasm
182    #[cfg(not(target_arch = "wasm32"))]
183    {
184        let mut tries: u16 = 0;
185        loop {
186            debug!("HTTP version: {:?}", response.version());
187            // loop if HTTP 429
188            if matches!(response.status(), StatusCode::TOO_MANY_REQUESTS) {
189                let retry_after_header = response
190                    .headers()
191                    .get(RETRY_AFTER)
192                    .map(|value| value.to_str().unwrap().to_string());
193                let retry_after = if let Some(rt) = retry_after_header {
194                    info!("Server says too many requests and to retry-after '{rt}'.");
195                    rt
196                } else {
197                    info!("Server says too many requests but does not offer 'retry-after' value.");
198                    client.request_options.def_retry_secs.to_string()
199                };
200                let mut wait_time_seconds =
201                    if let Ok(date) = DateTime::parse_from_rfc2822(&retry_after) {
202                        (date.with_timezone(&Utc) - Utc::now()).num_seconds() as u64
203                    } else if let Ok(seconds) = retry_after.parse::<u64>() {
204                        seconds
205                    } else {
206                        info!(
207                            "Unable to parse retry-after header value. Using {}",
208                            client.request_options.def_retry_secs
209                        );
210                        client.request_options.def_retry_secs.into()
211                    };
212                if wait_time_seconds == 0 {
213                    info!("Given {wait_time_seconds} for retry-after. Does not make sense.");
214                    wait_time_seconds = client.request_options.def_retry_secs as u64;
215                }
216                if wait_time_seconds > client.request_options.max_retry_secs as u64 {
217                    info!(
218                        "Server is asking to wait longer than configured max of {}.",
219                        client.request_options.max_retry_secs
220                    );
221                    wait_time_seconds = client.request_options.max_retry_secs as u64;
222                }
223                info!("Waiting {wait_time_seconds} seconds to retry.");
224                tokio::time::sleep(tokio::time::Duration::from_secs(wait_time_seconds + 1)).await;
225                tries += 1;
226                if tries > client.request_options.max_retries {
227                    info!("Max query retries reached.");
228                    break;
229                } else {
230                    // send the query again
231                    response = client.reqwest_client.get(url).send().await?;
232                }
233
234            // else don't repeat the request
235            } else {
236                break;
237            }
238        }
239    }
240
241    // throw an error if not 200 OK
242    let response = response.error_for_status()?;
243
244    // get the response
245    let content_type = response
246        .headers()
247        .get(CONTENT_TYPE)
248        .map(|value| value.to_str().unwrap().to_string());
249    let expires = response
250        .headers()
251        .get(EXPIRES)
252        .map(|value| value.to_str().unwrap().to_string());
253    let cache_control = response
254        .headers()
255        .get(CACHE_CONTROL)
256        .map(|value| value.to_str().unwrap().to_string());
257    let location = response
258        .headers()
259        .get(LOCATION)
260        .map(|value| value.to_str().unwrap().to_string());
261    let access_control_allow_origin = response
262        .headers()
263        .get(ACCESS_CONTROL_ALLOW_ORIGIN)
264        .map(|value| value.to_str().unwrap().to_string());
265    let strict_transport_security = response
266        .headers()
267        .get(STRICT_TRANSPORT_SECURITY)
268        .map(|value| value.to_str().unwrap().to_string());
269    let retry_after = response
270        .headers()
271        .get(RETRY_AFTER)
272        .map(|value| value.to_str().unwrap().to_string());
273    let content_length = response.content_length();
274    let status_code = response.status().as_u16();
275    let url = response.url().to_owned();
276    let text = response.text().await?;
277
278    let http_data = HttpData::now()
279        .status_code(status_code)
280        .and_location(location)
281        .and_content_length(content_length)
282        .and_content_type(content_type)
283        .scheme(url.scheme())
284        .host(
285            url.host_str()
286                .expect("URL has no host. This shouldn't happen.")
287                .to_owned(),
288        )
289        .and_expires(expires)
290        .and_cache_control(cache_control)
291        .and_access_control_allow_origin(access_control_allow_origin)
292        .and_strict_transport_security(strict_transport_security)
293        .and_retry_after(retry_after)
294        .build();
295
296    Ok(WrappedResponse { http_data, text })
297}