1use 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#[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#[derive(Default)]
42pub struct ClientConfig {
43 client_config: ReqwestClientConfig,
45
46 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
129pub struct Client {
131 pub(crate) reqwest_client: ReqwestClient,
133
134 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
147pub 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#[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 #[allow(unused_mut)] let mut response = client.reqwest_client.get(url).send().await?;
180
181 #[cfg(not(target_arch = "wasm32"))]
183 {
184 let mut tries: u16 = 0;
185 loop {
186 debug!("HTTP version: {:?}", response.version());
187 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 response = client.reqwest_client.get(url).send().await?;
232 }
233
234 } else {
236 break;
237 }
238 }
239 }
240
241 let response = response.error_for_status()?;
243
244 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}