Skip to main content

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}