1use std::{fmt, time::Duration};
2
3use base64::{Engine, engine::general_purpose::STANDARD as base64};
4
5use crate::http::header::{self, HeaderName, HeaderValue};
6use crate::http::{HeaderMap, error::HttpError};
7use crate::service::cfg::{CfgContext, Configuration};
8use crate::time::{Millis, Seconds};
9
10#[derive(Debug)]
11pub struct ClientConfig {
17 pub(super) headers: HeaderMap,
18 pub(super) timeout: Millis,
19 pub(super) pl_limit: usize,
20 pub(super) pl_timeout: Millis,
21 pub(super) h1_lifetime: Duration,
22 pub(super) h1_keep_alive: Duration,
23 pub(super) h1_limit: usize,
24 pub(super) h2_lifetime: Duration,
25 pub(super) h2_keep_alive: Duration,
26 pub(super) h2_limit: usize,
27 pub(super) h2_max_streams: u32,
28
29 config: CfgContext,
30}
31
32impl Default for ClientConfig {
33 fn default() -> Self {
34 Self::new()
35 }
36}
37
38impl Configuration for ClientConfig {
39 const NAME: &str = "Http client configuration";
40
41 fn ctx(&self) -> &CfgContext {
42 &self.config
43 }
44
45 fn set_ctx(&mut self, ctx: CfgContext) {
46 self.config = ctx;
47 }
48}
49
50impl ClientConfig {
51 #[must_use]
52 pub fn new() -> ClientConfig {
54 ClientConfig {
55 headers: HeaderMap::new(),
56 timeout: Millis(5_000),
57 pl_limit: 262_144,
58 pl_timeout: Millis(10_000),
59 h1_lifetime: Duration::from_secs(75),
60 h1_keep_alive: Duration::from_secs(15),
61 h1_limit: 8,
62 h2_lifetime: Duration::from_hours(1),
63 h2_keep_alive: Duration::from_mins(1),
64 h2_limit: 16,
65 h2_max_streams: 100,
66
67 config: CfgContext::default(),
68 }
69 }
70
71 pub fn headers(&self) -> &HeaderMap {
73 &self.headers
74 }
75
76 pub fn response_timeout(&self) -> Millis {
78 self.timeout
79 }
80
81 pub fn response_payload_limit(&self) -> usize {
85 self.pl_limit
86 }
87
88 pub fn response_payload_timeout(&self) -> Millis {
90 self.pl_timeout
91 }
92
93 pub fn h1_connection_limit(&self) -> usize {
95 self.h1_limit
96 }
97
98 pub fn h2_connection_limit(&self) -> usize {
100 self.h2_limit
101 }
102
103 pub fn h2_max_streams(&self) -> u32 {
105 self.h2_max_streams
106 }
107
108 pub fn h2_keepalive(&self) -> Seconds {
110 Seconds(self.h2_keep_alive.as_secs().try_into().unwrap_or(u16::MAX))
111 }
112
113 pub fn h2_lifetime(&self) -> Seconds {
115 Seconds(self.h2_lifetime.as_secs().try_into().unwrap_or(u16::MAX))
116 }
117
118 #[must_use]
119 pub fn set_h1_connection_limit(mut self, limit: usize) -> Self {
128 self.h1_limit = limit;
129 self
130 }
131
132 #[must_use]
133 pub fn set_h1_keepalive<T: Into<Seconds>>(mut self, dur: T) -> Self {
143 self.h1_keep_alive = dur.into().into();
144 self
145 }
146
147 #[must_use]
148 pub fn set_h1_lifetime<T: Into<Seconds>>(mut self, dur: T) -> Self {
156 self.h1_lifetime = dur.into().into();
157 self
158 }
159
160 #[must_use]
161 pub fn set_h2_connection_limit(mut self, limit: usize) -> Self {
175 self.h2_limit = limit;
176 self
177 }
178
179 #[must_use]
180 pub fn set_h2_max_streams(mut self, limit: u32) -> Self {
186 self.h2_max_streams = limit;
187 self
188 }
189
190 #[must_use]
191 pub fn set_h2_keepalive<T: Into<Seconds>>(mut self, dur: T) -> Self {
199 self.h2_keep_alive = dur.into().into();
200 self
201 }
202
203 #[must_use]
204 pub fn set_h2_lifetime<T: Into<Seconds>>(mut self, dur: T) -> Self {
211 self.h2_lifetime = dur.into().into();
212 self
213 }
214
215 #[must_use]
216 pub fn set_response_timeout<T: Into<Millis>>(mut self, timeout: T) -> Self {
222 self.timeout = timeout.into();
223 self
224 }
225
226 #[must_use]
227 pub fn disable_timeout(mut self) -> Self {
231 self.timeout = Millis::ZERO;
232 self
233 }
234
235 #[must_use]
236 pub fn set_response_payload_limit(mut self, limit: usize) -> Self {
240 self.pl_limit = limit;
241 self
242 }
243
244 #[must_use]
245 pub fn set_response_payload_timeout<T: Into<Millis>>(mut self, timeout: T) -> Self {
249 self.pl_timeout = timeout.into();
250 self
251 }
252
253 pub fn set_header<K, V>(mut self, key: K, value: V) -> Result<Self, HttpError>
257 where
258 HeaderName: TryFrom<K>,
259 HeaderValue: TryFrom<V>,
260 <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
261 <HeaderValue as TryFrom<V>>::Error: Into<HttpError>,
262 {
263 let key = HeaderName::try_from(key).map_err(Into::into)?;
264 let value = HeaderValue::try_from(value).map_err(Into::into)?;
265 self.headers.append(key, value);
266 Ok(self)
267 }
268
269 fn insert_header(mut self, key: HeaderName, value: String) -> Result<Self, HttpError> {
270 let value = HeaderValue::try_from(value).map_err(HttpError::from)?;
271 self.headers.insert(key, value);
272 Ok(self)
273 }
274
275 pub fn set_basic_auth<U>(self, username: U, password: Option<&str>) -> Result<Self, HttpError>
279 where
280 U: fmt::Display,
281 {
282 let auth = match password {
283 Some(password) => format!("{username}:{password}"),
284 None => format!("{username}:"),
285 };
286 self.insert_header(
287 header::AUTHORIZATION,
288 format!("Basic {}", base64.encode(auth)),
289 )
290 }
291
292 pub fn set_bearer_auth<T>(self, token: T) -> Result<Self, HttpError>
296 where
297 T: fmt::Display,
298 {
299 self.insert_header(header::AUTHORIZATION, format!("Bearer {token}"))
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn basics() {
309 let cfg = ClientConfig::new().disable_timeout();
310 assert_eq!(cfg.timeout, Millis::ZERO);
311 }
312
313 #[test]
314 fn h2_settings() {
315 let cfg = ClientConfig::new();
316 assert_eq!(cfg.h2_connection_limit(), 16);
317 assert_eq!(cfg.h2_max_streams(), 100);
318 assert_eq!(cfg.h2_keepalive(), Seconds(60));
319 assert_eq!(cfg.h2_lifetime(), Seconds(3600));
320
321 let cfg = cfg
322 .set_h2_connection_limit(2)
323 .set_h2_max_streams(10)
324 .set_h2_keepalive(Seconds(5))
325 .set_h2_lifetime(Seconds(50));
326 assert_eq!(cfg.h2_connection_limit(), 2);
327 assert_eq!(cfg.h2_max_streams(), 10);
328 assert_eq!(cfg.h2_keepalive(), Seconds(5));
329 assert_eq!(cfg.h2_lifetime(), Seconds(50));
330 assert_eq!(cfg.h1_connection_limit(), 8);
332 assert_eq!(cfg.h1_keep_alive, Duration::from_secs(15));
333 assert_eq!(cfg.h1_lifetime, Duration::from_secs(75));
334 }
335
336 #[test]
337 fn response_payload_limit() {
338 let cfg = ClientConfig::new();
339 assert_eq!(cfg.pl_limit, 262_144);
340
341 let cfg = cfg.set_response_payload_limit(10);
342 assert_eq!(cfg.pl_limit, 10);
343 }
344
345 #[test]
346 fn response_payload_timeout() {
347 let cfg = ClientConfig::default();
348 assert_eq!(cfg.pl_timeout, Millis(10_000));
349
350 let cfg = cfg.set_response_payload_timeout(Millis(10));
351 assert_eq!(cfg.pl_timeout, Millis(10));
352 }
353
354 #[test]
355 fn valid_header_name() {
356 let cfg = ClientConfig::new().set_header("Content-Length", 1).unwrap();
357 assert!(cfg.headers.contains_key("Content-Length"));
358 }
359
360 #[test]
361 fn invalid_header_name() {
362 let res = ClientConfig::new().set_header("no valid header name", 1);
363 assert!(res.is_err());
364 }
365
366 #[test]
367 fn valid_header_value() {
368 let valid_header_value = HeaderValue::from(1234);
369 let cfg = ClientConfig::new()
370 .set_header("Content-Length", &valid_header_value)
371 .unwrap();
372 assert_eq!(cfg.headers.get("Content-Length"), Some(&valid_header_value));
373 }
374
375 #[test]
376 fn invalid_header_value() {
377 let res = ClientConfig::new()
378 .set_header("Content-Length", "\n")
379 .is_err();
380 assert!(res);
381 }
382
383 #[test]
384 fn client_basic_auth() {
385 let cfg = ClientConfig::new()
386 .set_basic_auth("username", Some("password"))
387 .unwrap();
388 assert_eq!(
389 cfg.headers
390 .get(header::AUTHORIZATION)
391 .unwrap()
392 .to_str()
393 .unwrap(),
394 "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
395 );
396
397 let cfg = ClientConfig::new()
398 .set_basic_auth("username", None)
399 .unwrap();
400 assert_eq!(
401 cfg.headers
402 .get(header::AUTHORIZATION)
403 .unwrap()
404 .to_str()
405 .unwrap(),
406 "Basic dXNlcm5hbWU6"
407 );
408 }
409
410 #[test]
411 fn client_bearer_auth() {
412 let cfg = ClientConfig::new()
413 .set_bearer_auth("someS3cr3tAutht0k3n")
414 .unwrap();
415 assert_eq!(
416 cfg.headers
417 .get(header::AUTHORIZATION)
418 .unwrap()
419 .to_str()
420 .unwrap(),
421 "Bearer someS3cr3tAutht0k3n"
422 );
423 }
424}