asknothingx2_util/api/preset/mod.rs
1mod extra_config;
2
3pub use extra_config::{Http2Settings, SecurityProfile};
4pub use reqwest::tls;
5
6use std::time::Duration;
7
8use http::HeaderMap;
9use reqwest::{
10 Client, Proxy,
11 redirect::{self, Policy},
12};
13
14use super::{
15 HeaderMut,
16 error::{self, Error},
17};
18
19mod user_agents {
20 pub const DEFAULT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
21}
22
23/// HTTP client configuration preset with sensible defaults for various use cases.
24///
25/// **Default Configuration:**
26/// - Request timeout: 30s, Connect timeout: 10s
27/// - Connections: 20 max per host, 90s idle timeout
28/// - TLS: 1.2+ minimum, strict validation, HTTPS-only
29/// - Redirects: Up to 5 allowed
30/// - Cookies: Not saved, Referer: Sent
31/// - Compression: gzip enabled, brotli disabled
32#[doc = concat!("- User-Agent: ",
33 env!("CARGO_PKG_NAME"), "/",
34 env!("CARGO_PKG_VERSION"))]
35/// - HTTP/2: Auto-negotiated (supports both HTTP/1.1 and HTTP/2)
36/// - TCP keep-alive: Disabled
37///
38/// # Predefined Presets
39///
40/// Choose from these optimized configurations for common use cases:
41///
42/// - [`rest_api()`] - Balanced performance for REST API consumption (auto-negotiated HTTP/1.1 or HTTP/2)
43/// - [`authentication()`] - Secure settings for auth flows (HTTP/2 required)
44/// - [`low_latency()`] - Ultra-fast for time-sensitive operations (HTTP/2 required)
45/// - [`testing()`] - Permissive settings for test environments
46/// - [`debugging()`] - Maximum compatibility for development
47///
48/// # Examples
49///
50/// ```rust
51/// use std::time::Duration;
52/// use asknothingx2_util::api::preset::{self, Preset};
53///
54/// // Using a predefined preset
55/// let client = preset::rest_api("MyApp/1.0").build()?;
56///
57/// // Customizing with builder methods
58/// let mut preset = Preset::new();
59/// preset.user_agent("MyApp/1.0")
60/// .timeouts(Duration::from_secs(60), Duration::from_secs(5))
61/// .http2(true, None);
62///
63/// preset.build()?;
64/// # Ok::<(), Box<dyn std::error::Error>>(())
65/// ```
66///
67/// # Security Notes
68///
69/// See [`SecurityProfile`] for security-related configuration options and
70/// [`debug_mode()`](Self::debug_mode) for important warnings about insecure settings.
71#[derive(Debug)]
72pub struct Preset {
73 request_timeout: Duration,
74 connect_timeout: Duration,
75
76 pool_max_idle_per_host: usize,
77 pool_idle_timeout: Duration,
78 tcp_keepalive: Option<Duration>,
79 tcp_nodelay: bool,
80
81 minimum_tls_version: Option<tls::Version>,
82
83 allow_invalid_certificates: bool,
84 allow_wrong_hostnames: bool,
85 tls_sni: bool,
86
87 http2_prior_knowledge: bool,
88 http2_config: Option<Http2Settings>,
89
90 https_only: bool,
91
92 redirect: redirect::Policy,
93 save_cookies: bool,
94 send_referer: bool,
95
96 gzip: bool,
97 brotli: bool,
98
99 default_headers: HeaderMap,
100 user_agent: String,
101 proxy: Option<Proxy>,
102}
103
104impl Default for Preset {
105 fn default() -> Self {
106 Self {
107 request_timeout: Duration::from_secs(30),
108 connect_timeout: Duration::from_secs(10),
109 pool_max_idle_per_host: 20,
110 pool_idle_timeout: Duration::from_secs(90),
111 tcp_keepalive: None,
112 tcp_nodelay: true,
113 minimum_tls_version: Some(tls::Version::TLS_1_2),
114
115 allow_invalid_certificates: false,
116 allow_wrong_hostnames: false,
117 tls_sni: true,
118
119 http2_prior_knowledge: false,
120 http2_config: None,
121
122 https_only: true,
123
124 redirect: Policy::limited(5),
125 save_cookies: false,
126 send_referer: true,
127
128 gzip: true,
129 brotli: false,
130
131 default_headers: HeaderMap::new(),
132 user_agent: user_agents::DEFAULT.to_string(),
133 proxy: None,
134 }
135 }
136}
137
138impl Preset {
139 pub fn new() -> Self {
140 Preset::default()
141 }
142 /// Configure request and connection timeout durations.
143 ///
144 /// - `timeout`: Maximum time to wait for complete request (including response)
145 /// - `connect_timeout`: Maximum time to wait for initial connection establishment
146 ///
147 /// Use shorter timeouts for low-latency services, longer for slow/unreliable endpoints.
148 ///
149 pub fn timeouts(&mut self, timeout: Duration, connect_timeout: Duration) -> &mut Self {
150 self.request_timeout = timeout;
151 self.connect_timeout = connect_timeout;
152 self
153 }
154 /// Configure connection pool settings for HTTP keep-alive optimization.
155 ///
156 /// - `max`: Maximum idle connections to keep per host (higher = more reuse, more memory)
157 /// - `pool_idle_timeout`: How long to keep idle connections before closing
158 ///
159 /// Increase `max` for high-throughput scenarios, decrease for memory-constrained environments.
160 ///
161 pub fn connections(&mut self, max: usize, pool_idle_timeout: Duration) -> &mut Self {
162 self.pool_max_idle_per_host = max;
163 self.pool_idle_timeout = pool_idle_timeout;
164 self
165 }
166 /// Configure TCP keep-alive to detect dead connections.
167 ///
168 /// - `Some(duration)`: Send keep-alive probes every `duration` to detect dead connections
169 /// - `None`: Disable keep-alive (connections may hang on network issues)
170 ///
171 /// Enable for long-lived connections, disable for short-lived or high-churn scenarios.
172 ///
173 pub fn keepalive(&mut self, val: Option<Duration>) -> &mut Self {
174 self.tcp_keepalive = val;
175 self
176 }
177
178 /// Enable TCP Nagle's algorithm (default: disabled for lower latency).
179 ///
180 /// Nagle's algorithm batches small packets to improve network efficiency but increases latency.
181 /// Call this method only if you prioritize bandwidth over response time.
182 ///
183 pub fn tcp_delay(&mut self) -> &mut Self {
184 self.tcp_nodelay = false;
185 self
186 }
187
188 /// Set minimum required TLS version for secure connections.
189 pub fn min_tls(&mut self, version: tls::Version) -> &mut Self {
190 self.minimum_tls_version = Some(version);
191 self
192 }
193
194 /// Enable insecure TLS settings for testing/debugging (NEVER use in production).
195 ///
196 /// - `invalid_certificates`: Accept self-signed or expired certificates
197 /// - `wrong_hostnames`: Accept certificates for different hostnames
198 ///
199 pub fn debug_mode(&mut self, invalid_certificates: bool, wrong_hostnames: bool) -> &mut Self {
200 self.allow_invalid_certificates = invalid_certificates;
201 self.allow_wrong_hostnames = wrong_hostnames;
202 self
203 }
204
205 /// Configure HTTP/2 protocol settings for improved performance.
206 ///
207 /// - `prior`: Skip protocol negotiation and use HTTP/2 only (faster but HTTP/2-only)
208 /// - [`config`](Http2Settings): Custom HTTP/2 tuning parameters (None = use defaults)
209 ///
210 /// **Important:** When `prior = true`, HTTP/1.1 is NOT supported - connection fails if
211 /// server doesn't support HTTP/2. When `prior = false` (default), both HTTP/1.1 and
212 /// HTTP/2 are supported via protocol negotiation.
213 ///
214 pub fn http2(&mut self, prior: bool, config: Option<Http2Settings>) -> &mut Self {
215 self.http2_prior_knowledge = prior;
216 self.http2_config = config;
217 self
218 }
219
220 /// Allow HTTP connections in addition to HTTPS (reduces security).
221 ///
222 /// By default, only HTTPS is allowed. Call this method to also allow unencrypted HTTP.
223 /// Only use for testing, debugging, or when forced by legacy systems.
224 ///
225 pub fn disable_https_only(&mut self) -> &mut Self {
226 self.https_only = false;
227 self
228 }
229
230 /// Configure automatic redirect following behavior.
231 pub fn redirect(&mut self, policy: Policy) -> &mut Self {
232 self.redirect = policy;
233 self
234 }
235
236 /// Apply a security configuration preset that sets multiple security-related options.
237 pub fn security(&mut self, config: SecurityProfile) -> &mut Self {
238 self.save_cookies = config.save_cookies;
239 self.send_referer = config.send_referer;
240
241 self.minimum_tls_version = config.min_tls_version;
242
243 self.redirect = config.redirect;
244 self
245 }
246
247 /// Set the User-Agent header sent with all requests.
248 pub fn user_agent(&mut self, user_agent: impl Into<String>) -> &mut Self {
249 self.user_agent = user_agent.into();
250 self
251 }
252
253 /// Configure default headers sent with every request.
254 ///
255 /// # Example
256 ///
257 /// ```no_run
258 /// # use asknothingx2_util::api::preset::Preset;
259 /// # fn run() -> Result<(), asknothingx2_util::api::Error> {
260 /// let mut preset = Preset::new();
261 /// preset.default_headers_mut()
262 /// .accept_json()
263 /// .content_type_json()
264 /// .user_agent("user-agent/1.0")?;
265 /// # Ok(())
266 /// # }
267 /// ```
268 pub fn default_headers_mut(&mut self) -> HeaderMut<'_> {
269 HeaderMut::new(&mut self.default_headers)
270 }
271
272 /// Configure HTTP proxy for all requests through this client.
273 pub fn proxy(&mut self, proxy: Proxy) -> &mut Self {
274 self.proxy = Some(proxy);
275 self
276 }
277 /// Configure automatic response compression handling.
278 ///
279 /// - `gzip`
280 /// - `brotli`
281 ///
282 pub fn compressions(&mut self, gzip: bool, brotli: bool) -> &mut Self {
283 self.gzip = gzip;
284 self.brotli = brotli;
285 self
286 }
287}
288
289impl Preset {
290 /// Build the configured [`reqwest::Client`] from this preset.
291 pub fn build(self) -> Result<Client, Error> {
292 let mut builder = Client::builder()
293 .timeout(self.request_timeout)
294 .connect_timeout(self.connect_timeout)
295 .pool_max_idle_per_host(self.pool_max_idle_per_host)
296 .pool_idle_timeout(self.pool_idle_timeout)
297 .tcp_keepalive(self.tcp_keepalive)
298 .tcp_nodelay(self.tcp_nodelay)
299 .danger_accept_invalid_certs(self.allow_invalid_certificates)
300 .danger_accept_invalid_hostnames(self.allow_wrong_hostnames)
301 .tls_sni(self.tls_sni)
302 .redirect(self.redirect)
303 .cookie_store(self.save_cookies)
304 .referer(self.send_referer)
305 .gzip(self.gzip)
306 .brotli(self.brotli)
307 .user_agent(&self.user_agent)
308 .default_headers(self.default_headers)
309 .https_only(self.https_only)
310 .use_rustls_tls();
311
312 if let Some(version) = self.minimum_tls_version {
313 builder = builder.min_tls_version(version)
314 }
315
316 if self.http2_prior_knowledge {
317 builder = builder.http2_prior_knowledge();
318 }
319
320 if let Some(config) = self.http2_config {
321 builder = builder
322 .http2_initial_stream_window_size(config.initial_stream_window_size)
323 .http2_initial_connection_window_size(config.initial_connection_window_size)
324 .http2_max_frame_size(config.max_frame_size)
325 .http2_adaptive_window(config.adaptive_window);
326 }
327
328 if let Some(proxy) = self.proxy {
329 builder = builder.proxy(proxy);
330 }
331
332 builder.build().map_err(error::request::build)
333 }
334}
335
336/// Creates a basic HTTP client with standard defaults suitable for general-purpose requests.
337///
338/// **Configuration:**
339/// - Request timeout: 30s, Connect timeout: 10s
340/// - Connections: 20 max per host, 90s idle timeout
341/// - TLS: 1.2+ minimum, strict validation
342/// - HTTPS: Enforced (HTTP blocked)
343/// - HTTP/2: Auto-negotiated (supports both HTTP/1.1 and HTTP/2)
344/// - Redirects: Up to 5 allowed
345/// - Cookies: Not saved, Referer: Sent
346/// - Compression: gzip enabled, brotli disabled
347pub fn default(user_agent: &str) -> Preset {
348 let mut preset = Preset::default();
349 preset.user_agent(user_agent);
350 preset
351}
352
353/// Creates an HTTP client optimized for REST API consumption with balanced performance
354/// and reliability.
355///
356/// **Configuration:**
357/// - Request timeout: 30s, Connect timeout: 10s
358/// - Connections: 20 max per host, 90s idle timeout
359/// - TLS: 1.2+ minimum, strict validation
360/// - HTTPS: Enforced (HTTP blocked)
361/// - HTTP/2: Auto-negotiated (supports both HTTP/1.1 and HTTP/2)
362/// - Redirects: Up to 5 allowed
363/// - Cookies: Not saved, Referer: Not sent
364/// - Compression: gzip + brotli enabled
365/// - Headers: Accept JSON, standard encoding
366pub fn rest_api(user_agent: &str) -> Preset {
367 let mut preset = Preset::default();
368 preset.compressions(true, true).user_agent(user_agent);
369 preset
370 .default_headers_mut()
371 .accept_json()
372 .accept_encoding_standard();
373
374 preset
375}
376
377/// Creates an HTTP client configured for authentication flows and secure operations.
378///
379/// **Configuration:**
380/// - Request timeout: 60s, Connect timeout: 10s
381/// - Connections: 30 max per host, 90s idle timeout
382/// - TLS: 1.2+ minimum, strict validation
383/// - HTTPS: Enforced (HTTP blocked)
384/// - HTTP/2: Required (no HTTP/1.1 fallback)
385/// - Redirects: Up to 5 allowed
386/// - Cookies: Not saved, Referer: Sent
387/// - Headers: Accept JSON, no-cache control
388pub fn authentication(user_agent: &str) -> Preset {
389 let mut preset = Preset::default();
390 preset
391 .timeouts(Duration::from_secs(60), Duration::from_secs(10))
392 .connections(30, Duration::from_secs(90))
393 .http2(true, Some(Http2Settings::default()))
394 .user_agent(user_agent);
395
396 preset
397 .default_headers_mut()
398 .accept_json()
399 .cache_control_no_cache();
400
401 preset
402}
403
404/// Creates an HTTP client optimized for ultra-low latency and time-sensitive operations.
405///
406/// **Configuration:**
407/// - Request timeout: 3s, Connect timeout: 500ms
408/// - Connections: 100 max per host, 180s idle timeout
409/// - TLS: 1.2+ minimum, strict validation
410/// - HTTPS: Enforced (HTTP blocked)
411/// - HTTP/2: Required with custom tuning (no HTTP/1.1 fallback)
412/// - Redirects: Disabled
413/// - Cookies: Not saved, Referer: Not sent (strict security)
414/// - Compression: Disabled for minimal overhead
415/// - Headers: Accept JSON, no-cache control
416pub fn low_latency(user_agent: &str) -> Preset {
417 let mut preset = Preset::default();
418 preset
419 .timeouts(Duration::from_secs(3), Duration::from_millis(500))
420 .connections(100, Duration::from_secs(180))
421 .security(SecurityProfile::strict_1_2().redirect(Policy::none()))
422 .http2(
423 true,
424 Some(Http2Settings::new(65_536, 1_048_576, 16_384, false)),
425 )
426 .compressions(false, false)
427 .user_agent(user_agent);
428
429 preset
430 .default_headers_mut()
431 .accept_json()
432 .cache_control_no_cache();
433
434 preset
435}
436
437/// Creates an HTTP client with permissive settings for testing environments.
438///
439/// **Configuration:**
440/// - Request timeout: 10s, Connect timeout: 3s
441/// - Connections: 1 max per host, 5s idle timeout
442/// - TLS: Test security config, accepts invalid certificates
443/// - HTTPS: Not enforced (HTTP allowed)
444/// - Cookies: Not saved, Referer: Not sent
445/// - Debug mode: Accepts invalid certs and wrong hostnames
446/// - Headers: Accept JSON
447pub fn testing(user_agent: &str) -> Preset {
448 let mut preset = Preset::default();
449 preset
450 .timeouts(Duration::from_secs(10), Duration::from_secs(3))
451 .connections(1, Duration::from_secs(5))
452 .security(SecurityProfile::test())
453 .http2(false, None)
454 .user_agent(user_agent)
455 .disable_https_only()
456 .debug_mode(true, true);
457
458 preset.default_headers_mut().accept_json();
459
460 preset
461}
462
463/// Creates an HTTP client with maximum compatibility for debugging and development.
464///
465/// **Configuration:**
466/// - Request timeout: 300s (5min), Connect timeout: 30s
467/// - Connections: 1 max per host, 60s idle timeout
468/// - TLS: Debug security config, all validation disabled
469/// - HTTPS: Not enforced (HTTP allowed)
470/// - Cookies: Saved, Referer: Sent (maximum compatibility)
471/// - Debug mode: Accepts invalid certs and wrong hostnames
472/// - Headers: Accept any content type, no-cache control
473pub fn debugging(user_agent: &str) -> Preset {
474 let mut preset = Preset::default();
475 preset
476 .timeouts(Duration::from_secs(300), Duration::from_secs(30))
477 .connections(1, Duration::from_secs(60))
478 .security(SecurityProfile::debug())
479 .http2(false, None)
480 .user_agent(user_agent)
481 .disable_https_only()
482 .debug_mode(true, true);
483
484 preset
485 .default_headers_mut()
486 .accept_any()
487 .cache_control_no_cache();
488
489 preset
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495
496 #[test]
497 pub fn build() {
498 rest_api("rest-api/1.0").build().unwrap();
499 authentication("auth/1.0").build().unwrap();
500 low_latency("real-time/1.0").build().unwrap();
501 testing("test/1.0").build().unwrap();
502 debugging("debug/1.0").build().unwrap();
503 }
504}