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