1use std::{
6 net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
7 str::FromStr,
8 sync::{Arc, RwLock},
9 time::Duration,
10};
11
12use http::header::{HeaderMap, HeaderName, HeaderValue};
13
14#[cfg(feature = "cache")]
15use crate::{
16 client::{HttpCacheRecipe, HttpCacheStore},
17 options::CacheStore,
18};
19
20#[cfg(feature = "cache")]
21use http_cache_reqwest::{
22 CACacheManager, CacheOptions, HttpCacheOptions, MokaCacheBuilder, MokaManager,
23};
24use moka::sync::Cache as MokaCache;
25use reqwest::{Identity, tls::Certificate};
26
27#[cfg(feature = "connection-tracking")]
28use web_faith_conn_tracker::ConnectionTracker;
29
30#[cfg(feature = "cookies")]
31use web_faith_cookies::FaithJar;
32
33#[cfg(feature = "dns")]
34use web_faith_dns::{DEFAULT_MAX_STALE, FaithResolver, ResolverConfig, ServerSpec, parse_domains};
35
36#[cfg(feature = "http3")]
37use web_faith_alt_svc::{AltSvcCache, AltSvcCacheConfig};
38
39use crate::{
40 USER_AGENT,
41 agent::{Agent, AgentSettings, Live},
42 body::DrainPolicy,
43 client::{ClientRecipe, NodeEnvRecipe},
44 error::{FaithError, FaithErrorKind},
45 options::{AgentOptions, DnsOverride, Header, ipv6_wildcard_bindable, resolve_windows},
46 request::PRIORITY,
47};
48
49#[cfg(feature = "http3")]
50use crate::{client::H3UpgradeRecipe, options::Http3Congestion};
51
52#[cfg(all(feature = "http3", feature = "dns"))]
53use crate::client::install_https_sink;
54
55#[cfg(feature = "unstable-internals")]
58impl Agent {
59 pub fn from_options(options: AgentOptions) -> Result<Self, FaithError> {
61 Self::from_options_impl(options)
62 }
63}
64
65impl Agent {
66 pub(crate) fn from_options_impl(options: AgentOptions) -> Result<Self, FaithError> {
68 let AgentOptions {
72 #[cfg(feature = "cache")]
73 cache,
74 #[cfg(feature = "cookies")]
75 cookies,
76 dns,
77 flow_control,
78 headers,
79 http2,
80 #[cfg(feature = "http3")]
81 http3,
82 local_address,
83 pool,
84 quirks,
85 redirect,
86 timeout,
87 tls,
88 user_agent,
89 } = options;
90
91 let quirk_h1_request_streaming = quirks
92 .and_then(|quirks| quirks.h1_request_streaming)
93 .unwrap_or(false);
94
95 let local_address = match local_address {
101 Some(address) => Some(address),
102 None if !ipv6_wildcard_bindable() => Some(IpAddr::V4(Ipv4Addr::UNSPECIFIED)),
103 None => None,
104 };
105
106 #[cfg(feature = "cookies")]
110 let cookie_jar = cookies.map(|limits| Arc::new(FaithJar::new(limits)));
111
112 let dns = dns.unwrap_or_default();
113 #[cfg(feature = "dns")]
116 let dns_system = dns.system.unwrap_or(false);
117 #[cfg(feature = "dns")]
121 if dns_system
122 && dns
123 .servers
124 .as_ref()
125 .is_some_and(|servers| !servers.is_empty())
126 {
127 return Err(FaithError::new(
128 FaithErrorKind::Config,
129 "dns.servers cannot be combined with dns.system".to_string(),
130 ));
131 }
132 let dns_overrides = dns
136 .overrides
137 .unwrap_or_default()
138 .into_iter()
139 .map(|DnsOverride { domain, addresses }| {
140 let addresses = addresses
141 .into_iter()
142 .map(|addr| match SocketAddr::from_str(&addr) {
143 Ok(addr) => Ok(addr),
144 Err(err) => match IpAddr::from_str(&addr) {
145 Ok(IpAddr::V4(ip)) => Ok(SocketAddr::V4(SocketAddrV4::new(ip, 0))),
146 Ok(IpAddr::V6(ip)) => {
147 Ok(SocketAddr::V6(SocketAddrV6::new(ip, 0, 0, 0)))
148 }
149 Err(_) => Err(FaithError::new(
150 FaithErrorKind::AddressParse,
151 format!("{addr:?}: {err}"),
152 )),
153 },
154 })
155 .collect::<Result<Vec<_>, FaithError>>()?;
156 Ok((domain, addresses))
157 })
158 .collect::<Result<Vec<_>, FaithError>>()?;
159
160 #[cfg(feature = "dns")]
167 let dns_resolver = if dns_system {
168 None
169 } else {
170 let mut servers = Vec::new();
173 for url in dns.servers.unwrap_or_default() {
174 servers.push(url.parse::<ServerSpec>().map_err(|err| {
175 FaithError::new(FaithErrorKind::AddressParse, format!("{url:?}: {err}"))
176 })?);
177 }
178 Some(FaithResolver::new(ResolverConfig {
179 servers,
180 timeout: dns.timeout.map(|ms| Duration::from_millis(ms.into())),
181 ndots: dns.ndots.map(|n| n as usize),
182 search_domains: parse_domains(dns.search_domains)
183 .map_err(|message| FaithError::new(FaithErrorKind::Config, message))?,
184 hosts_file: dns.hosts_file,
185 exempt_domains: parse_domains(dns.exempt_domains)
186 .map_err(|message| FaithError::new(FaithErrorKind::Config, message))?
187 .unwrap_or_default(),
188 serve_stale: dns.serve_stale.unwrap_or(true).then(|| {
191 dns.max_stale
192 .map_or(DEFAULT_MAX_STALE, |ms| Duration::from_millis(ms.into()))
193 }),
194 }))
195 };
196
197 #[cfg(feature = "encoding")]
198 let mut default_accept_encoding = None;
199 #[cfg(feature = "encoding")]
200 let mut default_content_encoding = None;
201 let mut has_default_content_type = false;
202 let mut has_default_priority = false;
203 let mut default_headers = None;
204 if let Some(headers) = headers
205 && !headers.is_empty()
206 {
207 let map = HeaderMap::from_iter(headers.into_iter().filter_map(
208 |Header {
209 name,
210 value,
211 sensitive,
212 }| {
213 let Ok(name) = HeaderName::from_bytes(name.as_bytes()) else {
214 return None;
215 };
216
217 let Ok(mut value) = HeaderValue::from_bytes(value.as_bytes()) else {
218 return None;
219 };
220
221 if sensitive.unwrap_or(false) {
222 value.set_sensitive(true);
223 }
224
225 Some((name, value))
226 },
227 ));
228 #[cfg(feature = "encoding")]
229 {
230 default_accept_encoding = map.get(reqwest::header::ACCEPT_ENCODING).cloned();
231 default_content_encoding = map.get(reqwest::header::CONTENT_ENCODING).cloned();
232 }
233 has_default_content_type = map.contains_key(reqwest::header::CONTENT_TYPE);
234 has_default_priority = map.contains_key(PRIORITY);
235 default_headers = Some(map);
236 }
237
238 let http2 = http2.unwrap_or_default();
243 let http2_adaptive_window = http2.adaptive_window.unwrap_or(false);
244 let http2_windows = (!http2_adaptive_window).then(|| {
245 resolve_windows(
246 flow_control.as_ref(),
247 http2.stream_window,
248 http2.connection_window,
249 )
250 });
251
252 #[cfg(feature = "http3")]
253 let http3_max_idle_timeout = Duration::from_secs(
254 http3
255 .as_ref()
256 .and_then(|h| h.max_idle_timeout)
257 .unwrap_or(30)
258 .clamp(1, 120)
259 .into(),
260 );
261
262 #[cfg(feature = "http3")]
267 let http3_windows = resolve_windows(
268 flow_control.as_ref(),
269 http3.as_ref().and_then(|h| h.stream_window),
270 http3.as_ref().and_then(|h| h.connection_window),
271 );
272
273 #[cfg(feature = "http3")]
274 let http3_congestion_bbr = matches!(
275 http3.as_ref().and_then(|h| h.congestion),
276 Some(Http3Congestion::Bbr1)
277 );
278
279 #[cfg(feature = "http3")]
280 let http3_send_window = http3.as_ref().and_then(|h| h.send_window);
281
282 let pool_idle_timeout = pool
283 .as_ref()
284 .and_then(|pool| pool.idle_timeout)
285 .map(|seconds| Duration::from_secs(seconds.into()));
286 let pool_max_idle_per_host = pool.as_ref().map(|pool| {
288 pool.max_idle_per_host
289 .and_then(|n| n.try_into().ok())
290 .unwrap_or(usize::MAX)
291 });
292
293 let drain = {
295 let default = DrainPolicy::default();
296 DrainPolicy {
297 limit: pool
298 .as_ref()
299 .and_then(|pool| pool.drain_limit)
300 .map_or(default.limit, u64::from),
301 timeout: pool
302 .as_ref()
303 .and_then(|pool| pool.drain_timeout)
304 .map_or(default.timeout, |millis| {
305 Duration::from_millis(millis.into())
306 }),
307 }
308 };
309
310 let connect_timeout = timeout
311 .and_then(|t| t.connect)
312 .map(|millis| Duration::from_millis(millis.into()));
313 let read_timeout = timeout
314 .and_then(|t| t.read)
315 .map(|millis| Duration::from_millis(millis.into()));
316 let total_timeout = timeout
317 .and_then(|t| t.total)
318 .map(|millis| Duration::from_millis(millis.into()));
319
320 #[cfg(feature = "http3")]
321 let tls_early_data = tls.as_ref().and_then(|tls| tls.early_data);
322 let tls_required = tls.as_ref().and_then(|tls| tls.required);
323 let (tls_identity, tls_extra_roots) = match tls {
326 None => (None, Vec::new()),
327 Some(tls) => {
328 let identity = match &tls.identity {
329 None => None,
330 Some(identity) => Some(Identity::from_pem(identity).map_err(|err| {
331 FaithError::new(FaithErrorKind::PemParse, err.to_string())
332 })?),
333 };
334
335 let mut extra_roots = Vec::new();
336 for pem in tls.extra_roots.iter().flatten() {
337 extra_roots.extend(Certificate::from_pem_bundle(pem).map_err(|err| {
338 FaithError::new(FaithErrorKind::PemParse, err.to_string())
339 })?);
340 }
341
342 (identity, extra_roots)
343 }
344 };
345
346 #[cfg(feature = "cache")]
347 let http_cache = if let Some(cache) = cache
348 && let Some(store) = cache.store
349 {
350 let mode = cache.mode.unwrap_or_default().into();
351 let options = HttpCacheOptions {
352 cache_options: Some(CacheOptions {
353 shared: cache.shared.unwrap_or(true),
354 ignore_cargo_cult: true,
355 ..Default::default()
356 }),
357 ..Default::default()
358 };
359 let store = match store {
360 CacheStore::Disk => HttpCacheStore::Disk(CACacheManager {
361 path: cache
362 .path
363 .ok_or_else(|| {
364 FaithError::new(FaithErrorKind::Config, "missing cache.path")
365 })?
366 .into(),
367 remove_opts: Default::default(),
368 }),
369 CacheStore::Memory => HttpCacheStore::Memory(MokaManager::new(
370 MokaCacheBuilder::new(cache.capacity.map_or(10_000, |n| n.into())).build(),
371 )),
372 };
373
374 Some(HttpCacheRecipe {
375 mode,
376 options,
377 store,
378 })
379 } else {
380 None
381 };
382
383 #[cfg(feature = "http3")]
386 let h3_follow_advertised_port = http3
387 .as_ref()
388 .and_then(|o| o.upgrade_follow_advertised_port)
389 .unwrap_or(false);
390 #[cfg(not(feature = "http3"))]
391 let h3_follow_advertised_port = false;
392
393 #[cfg(feature = "http3")]
396 let (alt_svc_cache, h3_upgrade) = {
397 let http3_opts = http3.as_ref();
398 let enabled = http3_opts.and_then(|o| o.upgrade_enabled).unwrap_or(true);
399
400 let advertised_ttl = Duration::from_secs(
401 http3_opts
402 .and_then(|o| o.upgrade_advertised_ttl)
403 .unwrap_or(86400)
404 .into(),
405 );
406 let confirmed_ttl = Duration::from_secs(
407 http3_opts
408 .and_then(|o| o.upgrade_confirmed_ttl)
409 .unwrap_or(86400)
410 .into(),
411 );
412 let failed_ttl = Duration::from_secs(
413 http3_opts
414 .and_then(|o| o.upgrade_failed_ttl)
415 .unwrap_or(300)
416 .into(),
417 );
418 let failed_max_ttl = Duration::from_secs(
419 http3_opts
420 .and_then(|o| o.upgrade_failed_max_ttl)
421 .unwrap_or(3600)
422 .into(),
423 );
424 let capacity = http3_opts
425 .and_then(|o| o.upgrade_cache_capacity)
426 .unwrap_or(10_000)
427 .into();
428 let cancel_strikes = http3_opts
429 .and_then(|o| o.upgrade_cancel_strikes)
430 .unwrap_or(3);
431 let attempt_timeout = match http3_opts
432 .and_then(|o| o.upgrade_attempt_timeout)
433 .unwrap_or(60_000)
434 {
435 0 => None,
436 millis => Some(Duration::from_millis(millis.into())),
437 };
438 let probe = http3_opts.and_then(|o| o.upgrade_probe).unwrap_or(true);
439 let probe_timeout = match http3_opts
440 .and_then(|o| o.upgrade_probe_timeout)
441 .unwrap_or(5_000)
442 {
443 0 => None,
444 millis => Some(Duration::from_millis(millis.into())),
445 };
446 let slow_factor = http3_opts
447 .and_then(|o| o.upgrade_slow_factor)
448 .unwrap_or(2.5);
449 let slow_ttl = Duration::from_secs(
450 http3_opts
451 .and_then(|o| o.upgrade_slow_ttl)
452 .unwrap_or(600)
453 .into(),
454 );
455
456 let cache = Arc::new(AltSvcCache::new(AltSvcCacheConfig {
457 advertised_ttl,
458 confirmed_ttl,
459 failed_ttl,
460 failed_max_ttl,
461 capacity,
462 cancel_strikes,
463 strike_window: Duration::from_secs(60),
464 follow_advertised_port: h3_follow_advertised_port,
465 probe_ttl: probe_timeout
469 .map_or(Duration::from_secs(125), |t| t + Duration::from_secs(5)),
470 slow_factor,
471 slow_ttl,
472 }));
473
474 if let Some(hints) = http3_opts.and_then(|o| o.hints.as_ref()) {
475 for hint in hints {
476 cache.add_hint(&hint.host, hint.port);
477 }
478 }
479
480 (
481 Some(cache),
482 H3UpgradeRecipe {
483 enabled,
484 attempt_timeout,
485 probe,
486 probe_timeout,
487 },
488 )
489 };
490
491 let recipe = ClientRecipe {
492 user_agent: user_agent.unwrap_or_else(|| USER_AGENT.to_owned()),
493 local_address,
494 default_headers,
495 #[cfg(feature = "dns")]
496 dns_system,
497 dns_overrides,
498 http2_adaptive_window,
499 http2_windows,
500 #[cfg(feature = "http3")]
501 http3_max_idle_timeout,
502 #[cfg(feature = "http3")]
503 http3_windows,
504 #[cfg(feature = "http3")]
505 http3_congestion_bbr,
506 #[cfg(feature = "http3")]
507 http3_send_window,
508 pool_idle_timeout,
509 pool_max_idle_per_host,
510 redirect,
511 connect_timeout,
512 read_timeout,
513 total_timeout,
514 #[cfg(feature = "http3")]
515 tls_early_data,
516 tls_identity,
517 tls_required,
518 tls_extra_roots,
519 node_env: NodeEnvRecipe::read(),
520 #[cfg(feature = "cache")]
521 http_cache,
522 #[cfg(feature = "http3")]
523 h3_upgrade,
524 };
525
526 let settings = AgentSettings {
527 h3_follow_advertised_port,
528 quirk_h1_request_streaming,
529 drain,
530 #[cfg(feature = "encoding")]
531 default_accept_encoding,
532 #[cfg(feature = "encoding")]
533 default_content_encoding,
534 has_default_content_type,
535 has_default_priority,
536 };
537
538 Self::build(
539 recipe,
540 settings,
541 #[cfg(feature = "cookies")]
542 cookie_jar,
543 #[cfg(feature = "dns")]
544 dns_resolver,
545 #[cfg(feature = "http3")]
546 alt_svc_cache,
547 )
548 }
549
550 pub fn new() -> Result<Self, FaithError> {
552 Self::from_options_impl(AgentOptions::default())
553 }
554
555 pub fn builder() -> crate::agent::AgentOptionsBuilder {
573 crate::options::AgentOptions::builder()
574 }
575
576 pub(crate) fn build(
581 recipe: ClientRecipe,
582 settings: AgentSettings,
583 #[cfg(feature = "cookies")] cookie_jar: Option<Arc<FaithJar>>,
584 #[cfg(feature = "dns")] dns_resolver: Option<FaithResolver>,
585 #[cfg(feature = "http3")] alt_svc_cache: Option<Arc<AltSvcCache>>,
586 ) -> Result<Self, FaithError> {
587 let conn_timeout = recipe.conn_timeout();
588 let built = recipe.build(
589 #[cfg(feature = "cookies")]
590 cookie_jar.as_ref(),
591 #[cfg(feature = "dns")]
592 dns_resolver.as_ref(),
593 #[cfg(feature = "http3")]
594 alt_svc_cache.as_ref(),
595 )?;
596
597 #[cfg(all(feature = "http3", feature = "dns"))]
601 install_https_sink(
602 dns_resolver.as_ref(),
603 alt_svc_cache.as_ref(),
604 built.prober.as_ref(),
605 recipe.h3_upgrade.enabled,
606 );
607
608 Ok(Self {
609 live: Arc::new(RwLock::new(Some(Live {
610 client: built.client,
611 raw_client: built.raw_client,
612 #[cfg(feature = "dns")]
613 dns_resolver,
614 #[cfg(feature = "http3")]
615 alt_svc_cache,
616 #[cfg(feature = "http3")]
617 h3_prober: built.prober,
618 }))),
619 warmed: MokaCache::builder().time_to_live(conn_timeout).build(),
622 warming: MokaCache::builder()
625 .time_to_live(Duration::from_secs(300))
626 .build(),
627 warm_generation: Default::default(),
628 #[cfg(feature = "cookies")]
629 cookie_jar,
630 stats: Default::default(),
631 #[cfg(feature = "connection-tracking")]
632 conn_tracker: ConnectionTracker::new(conn_timeout),
633 h3_follow_advertised_port: settings.h3_follow_advertised_port,
634 #[cfg(feature = "http3")]
635 h3_upgrade_enabled: recipe.h3_upgrade.enabled,
636 quirk_h1_request_streaming: settings.quirk_h1_request_streaming,
637 drain: settings.drain,
638 #[cfg(feature = "encoding")]
639 default_accept_encoding: settings.default_accept_encoding,
640 #[cfg(feature = "encoding")]
641 default_content_encoding: settings.default_content_encoding,
642 has_default_content_type: settings.has_default_content_type,
643 has_default_priority: settings.has_default_priority,
644 recipe: Arc::new(recipe),
645 })
646 }
647}