1#![allow(mismatched_lifetime_syntaxes)]
6
7use std::collections::HashSet;
8
9pub use reqwest::{
10 header::{self, HeaderValue},
11 Client as ReqwestClient, Error as ReqwestError,
12};
13
14use icann_rdap_common::{
15 media_types::{JSON_MEDIA_TYPE, RDAP_MEDIA_TYPE},
16 prelude::ExtensionId,
17};
18
19#[cfg(not(target_arch = "wasm32"))]
20use {icann_rdap_common::VERSION, std::net::SocketAddr, std::time::Duration};
21
22#[derive(Clone)]
24pub struct ReqwestClientConfig {
25 pub user_agent_suffix: String,
31
32 pub https_only: bool,
36
37 pub accept_invalid_host_names: bool,
41
42 pub accept_invalid_certificates: bool,
46
47 pub follow_redirects: bool,
51
52 pub host: Option<HeaderValue>,
54
55 pub origin: Option<HeaderValue>,
59
60 pub timeout_secs: u64,
66
67 pub exts_list: HashSet<ExtensionId>,
71}
72
73impl Default for ReqwestClientConfig {
74 fn default() -> Self {
75 Self {
76 user_agent_suffix: "library".to_string(),
77 https_only: true,
78 accept_invalid_host_names: false,
79 accept_invalid_certificates: false,
80 follow_redirects: true,
81 host: None,
82 origin: None,
83 timeout_secs: 60,
84 exts_list: HashSet::default(),
85 }
86 }
87}
88
89#[buildstructor::buildstructor]
90impl ReqwestClientConfig {
91 #[builder]
92 pub fn new(
93 user_agent_suffix: Option<String>,
94 https_only: Option<bool>,
95 accept_invalid_host_names: Option<bool>,
96 accept_invalid_certificates: Option<bool>,
97 follow_redirects: Option<bool>,
98 host: Option<HeaderValue>,
99 origin: Option<HeaderValue>,
100 timeout_secs: Option<u64>,
101 exts_list: Option<HashSet<ExtensionId>>,
102 ) -> Self {
103 let default = Self::default();
104 Self {
105 user_agent_suffix: user_agent_suffix.unwrap_or(default.user_agent_suffix),
106 https_only: https_only.unwrap_or(default.https_only),
107 accept_invalid_host_names: accept_invalid_host_names
108 .unwrap_or(default.accept_invalid_host_names),
109 accept_invalid_certificates: accept_invalid_certificates
110 .unwrap_or(default.accept_invalid_certificates),
111 follow_redirects: follow_redirects.unwrap_or(default.follow_redirects),
112 host,
113 origin,
114 timeout_secs: timeout_secs.unwrap_or(default.timeout_secs),
115 exts_list: exts_list.unwrap_or_default(),
116 }
117 }
118
119 #[builder(entry = "from_config", exit = "build")]
120 pub fn new_from_config(
121 &self,
122 user_agent_suffix: Option<String>,
123 https_only: Option<bool>,
124 accept_invalid_host_names: Option<bool>,
125 accept_invalid_certificates: Option<bool>,
126 follow_redirects: Option<bool>,
127 host: Option<HeaderValue>,
128 origin: Option<HeaderValue>,
129 timeout_secs: Option<u64>,
130 exts_list: Option<HashSet<ExtensionId>>,
131 ) -> Self {
132 Self {
133 user_agent_suffix: user_agent_suffix.unwrap_or(self.user_agent_suffix.clone()),
134 https_only: https_only.unwrap_or(self.https_only),
135 accept_invalid_host_names: accept_invalid_host_names
136 .unwrap_or(self.accept_invalid_host_names),
137 accept_invalid_certificates: accept_invalid_certificates
138 .unwrap_or(self.accept_invalid_certificates),
139 follow_redirects: follow_redirects.unwrap_or(self.follow_redirects),
140 host: host.map_or(self.host.clone(), Some),
141 origin: origin.map_or(self.origin.clone(), Some),
142 timeout_secs: timeout_secs.unwrap_or(self.timeout_secs),
143 exts_list: exts_list.unwrap_or(self.exts_list.clone()),
144 }
145 }
146}
147
148#[cfg(not(target_arch = "wasm32"))]
153pub fn create_reqwest_client(config: &ReqwestClientConfig) -> Result<ReqwestClient, ReqwestError> {
154 let default_headers = default_headers(config);
155
156 let mut client = reqwest::Client::builder();
157
158 let redirects = if config.follow_redirects {
159 reqwest::redirect::Policy::default()
160 } else {
161 reqwest::redirect::Policy::none()
162 };
163 client = client
164 .timeout(Duration::from_secs(config.timeout_secs))
165 .user_agent(format!(
166 "icann_rdap client {VERSION} {}",
167 config.user_agent_suffix
168 ))
169 .redirect(redirects)
170 .https_only(config.https_only)
171 .danger_accept_invalid_hostnames(config.accept_invalid_host_names)
172 .danger_accept_invalid_certs(config.accept_invalid_certificates);
173
174 let client = client.default_headers(default_headers).build()?;
175 Ok(client)
176}
177
178#[cfg(not(target_arch = "wasm32"))]
183pub fn create_reqwest_client_with_addr(
184 config: &ReqwestClientConfig,
185 domain: &str,
186 addr: SocketAddr,
187) -> Result<ReqwestClient, ReqwestError> {
188 let default_headers = default_headers(config);
189
190 let mut client = reqwest::Client::builder();
191
192 let redirects = if config.follow_redirects {
193 reqwest::redirect::Policy::default()
194 } else {
195 reqwest::redirect::Policy::none()
196 };
197 client = client
198 .timeout(Duration::from_secs(config.timeout_secs))
199 .user_agent(format!(
200 "icann_rdap client {VERSION} {}",
201 config.user_agent_suffix
202 ))
203 .redirect(redirects)
204 .https_only(config.https_only)
205 .danger_accept_invalid_hostnames(config.accept_invalid_host_names)
206 .danger_accept_invalid_certs(config.accept_invalid_certificates)
207 .resolve(domain, addr);
208
209 let client = client.default_headers(default_headers).build()?;
210 Ok(client)
211}
212
213#[cfg(target_arch = "wasm32")]
220pub fn create_reqwest_client(config: &ReqwestClientConfig) -> Result<ReqwestClient, ReqwestError> {
221 let default_headers = default_headers(config);
222
223 let client = reqwest::Client::builder();
224
225 let client = client.default_headers(default_headers).build()?;
226 Ok(client)
227}
228
229fn default_headers(config: &ReqwestClientConfig) -> header::HeaderMap {
230 let mut default_headers = header::HeaderMap::new();
231 let accept_media_types = if config.exts_list.is_empty() {
232 format!("{RDAP_MEDIA_TYPE}, {JSON_MEDIA_TYPE}")
233 } else {
234 let mut exts_list: Vec<String> = config.exts_list.iter().map(|e| e.to_string()).collect();
235 exts_list.sort();
236 let exts_list_param = exts_list.join(" ");
237 format!("{RDAP_MEDIA_TYPE};exts_list=\"{exts_list_param}\", {JSON_MEDIA_TYPE}")
238 };
239 let accept_value = HeaderValue::from_str(&accept_media_types).unwrap();
243 default_headers.insert(header::ACCEPT, accept_value);
244 if let Some(host) = &config.host {
245 default_headers.insert(header::HOST, host.into());
246 };
247 if let Some(origin) = &config.origin {
248 default_headers.insert(header::ORIGIN, origin.into());
249 }
250 default_headers
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use icann_rdap_common::prelude::ExtensionId;
257 use std::collections::HashSet;
258
259 #[test]
260 fn test_default_headers_empty_exts_list() {
261 let config = ReqwestClientConfig {
263 exts_list: HashSet::new(),
264 ..Default::default()
265 };
266
267 let headers = default_headers(&config);
269 let accept_header = headers.get(header::ACCEPT).unwrap();
270
271 let expected = format!("{RDAP_MEDIA_TYPE}, {JSON_MEDIA_TYPE}");
273 assert_eq!(accept_header.to_str().unwrap(), expected);
274 }
275
276 #[test]
277 fn test_default_headers_with_exts_list() {
278 let mut exts_list = HashSet::new();
280 exts_list.insert(ExtensionId::Cidr0);
281 exts_list.insert(ExtensionId::JsContact);
282 let config = ReqwestClientConfig {
283 exts_list,
284 ..Default::default()
285 };
286
287 let headers = default_headers(&config);
289 let accept_header = headers.get(header::ACCEPT).unwrap();
290
291 let expected =
293 format!("{RDAP_MEDIA_TYPE};exts_list=\"cidr0 jscontact\", {JSON_MEDIA_TYPE}");
294 assert_eq!(accept_header.to_str().unwrap(), expected);
295 }
296
297 #[test]
298 fn test_default_headers_single_extension() {
299 let mut exts_list = HashSet::new();
301 exts_list.insert(ExtensionId::Redacted);
302 let config = ReqwestClientConfig {
303 exts_list,
304 ..Default::default()
305 };
306
307 let headers = default_headers(&config);
309 let accept_header = headers.get(header::ACCEPT).unwrap();
310
311 let expected = format!("{RDAP_MEDIA_TYPE};exts_list=\"redacted\", {JSON_MEDIA_TYPE}");
313 assert_eq!(accept_header.to_str().unwrap(), expected);
314 }
315
316 #[test]
317 fn test_default_headers_with_host_and_origin() {
318 let mut exts_list = HashSet::new();
320 exts_list.insert(ExtensionId::Sorting);
321 let config = ReqwestClientConfig {
322 host: Some(HeaderValue::from_static("example.com")),
323 origin: Some(HeaderValue::from_static("https://example.com")),
324 exts_list,
325 ..Default::default()
326 };
327
328 let headers = default_headers(&config);
330
331 let accept_header = headers.get(header::ACCEPT).unwrap();
334 let expected = format!("{RDAP_MEDIA_TYPE};exts_list=\"sorting\", {JSON_MEDIA_TYPE}");
335 assert_eq!(accept_header.to_str().unwrap(), expected);
336
337 let host_header = headers.get(header::HOST).unwrap();
339 assert_eq!(host_header, "example.com");
340
341 let origin_header = headers.get(header::ORIGIN).unwrap();
343 assert_eq!(origin_header, "https://example.com");
344 }
345}