Skip to main content

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}