1use std::fmt;
2use std::io::{self, Read};
3use std::sync::atomic::AtomicBool;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use serde::de::DeserializeOwned;
8use serde::Serialize;
9
10use crate::cancel::{is_cancelled_io, is_set, sleep_cancellable, CancelToken, CheckedReader};
11use crate::error::{HttpError, Phase, MAX_ERROR_BODY_BYTES};
12use crate::netpolicy::{denial_in_chain, normalize_host, AddressPolicy, VettingResolver, ViaProxy};
13use crate::redirect;
14use crate::retry::{parse_retry_after, RetryPolicy};
15use crate::sse::SseReader;
16use crate::urlredact::{redact_url, redact_urls_in_text, url_secret_parts};
17
18pub(crate) const REDACTED: &str = "[redacted]";
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Method {
22 Get,
23 Post,
24 Put,
25 Patch,
26 Delete,
27 Head,
28}
29
30impl Method {
31 pub fn as_str(self) -> &'static str {
32 match self {
33 Method::Get => "GET",
34 Method::Post => "POST",
35 Method::Put => "PUT",
36 Method::Patch => "PATCH",
37 Method::Delete => "DELETE",
38 Method::Head => "HEAD",
39 }
40 }
41 pub fn is_idempotent(self) -> bool {
43 !matches!(self, Method::Post | Method::Patch)
44 }
45}
46
47#[derive(Debug, Clone)]
50pub struct RetryEvent {
51 pub url: String,
52 pub retry: u32,
54 pub reason: String,
55 pub delay: Duration,
56}
57
58type RetryObserver = Arc<dyn Fn(&RetryEvent) + Send + Sync>;
59
60#[derive(Debug, Clone)]
63pub struct AttemptEvent {
64 pub url: String,
65 pub method: Method,
66 pub attempt: u32,
68 pub max_attempts: u32,
70}
71
72type AttemptHook = Arc<dyn Fn(&AttemptEvent) -> Result<(), String> + Send + Sync>;
74
75#[derive(Clone, Default)]
102pub enum ProxyMode {
103 #[default]
105 None,
106 Environment,
108 Explicit(String),
110 System,
134 SystemForUrl(String),
152}
153
154impl fmt::Debug for ProxyMode {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 match self {
157 ProxyMode::None => f.write_str("None"),
158 ProxyMode::Environment => f.write_str("Environment"),
159 ProxyMode::Explicit(url) => write!(f, "Explicit({})", redact_url(url)),
160 ProxyMode::System => f.write_str("System"),
161 ProxyMode::SystemForUrl(url) => write!(f, "SystemForUrl({})", redact_url(url)),
162 }
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
183pub enum TrustRoots {
184 #[default]
188 Platform,
189 Bundled,
193}
194
195fn blocking_tls_config(trust: TrustRoots) -> ureq::tls::TlsConfig {
198 let roots = match trust {
199 TrustRoots::Platform => ureq::tls::RootCerts::PlatformVerifier,
200 TrustRoots::Bundled => {
201 let extra = crate::trust::test_roots();
202 if extra.is_empty() {
203 ureq::tls::RootCerts::WebPki
204 } else {
205 ureq::tls::RootCerts::Specific(std::sync::Arc::new(
206 extra
207 .iter()
208 .map(|d| ureq::tls::Certificate::from_der(d).to_owned())
209 .collect(),
210 ))
211 }
212 }
213 };
214 ureq::tls::TlsConfig::builder()
215 .provider(ureq::tls::TlsProvider::Rustls)
216 .root_certs(roots)
217 .build()
218}
219
220impl TrustRoots {
221 pub fn verifier_kind(self) -> &'static str {
223 match self {
224 TrustRoots::Platform => "platform",
225 TrustRoots::Bundled => "bundled",
226 }
227 }
228}
229
230pub(crate) struct ResolvedProxy {
232 pub(crate) host: String,
233 pub(crate) port: u16,
234 pub(crate) ureq: ureq::Proxy,
235 #[cfg_attr(not(feature = "async"), allow(dead_code))]
237 pub(crate) url: String,
238 #[allow(dead_code)]
240 pub(crate) no_proxy: Vec<String>,
241 pub(crate) from_env: bool,
243}
244
245#[derive(Clone, Debug, PartialEq, Eq)]
249pub enum ProxyModeUsed {
250 Direct,
253 Environment,
256 Explicit,
258 System,
260 SystemFallbackEnvironment { reason: String },
263 SystemFallbackDirect { reason: String },
265 SystemForUrl,
268}
269
270#[derive(Clone, Debug, PartialEq, Eq)]
273pub struct ProxyDiagnostic {
274 pub endpoint: String,
276 pub from_environment: bool,
279 pub endpoint_exempt_from_class_check: bool,
283}
284
285impl ProxyDiagnostic {
286 pub(crate) fn of(p: &ResolvedProxy) -> Self {
287 Self {
288 endpoint: format!("{}://{}:{}", p.scheme(), p.host, p.port),
289 from_environment: p.from_env,
290 endpoint_exempt_from_class_check: true,
291 }
292 }
293}
294
295impl ResolvedProxy {
296 fn scheme(&self) -> &'static str {
297 if self.ureq.protocol() == ureq::ProxyProtocol::Https {
298 "https"
299 } else {
300 "http"
301 }
302 }
303}
304
305const PROXY_ENV: [&str; 6] = [
306 "ALL_PROXY",
307 "all_proxy",
308 "HTTPS_PROXY",
309 "https_proxy",
310 "HTTP_PROXY",
311 "http_proxy",
312];
313
314fn build_proxy(url: &str, no_proxy: &[String]) -> Result<ResolvedProxy, HttpError> {
316 let invalid = || HttpError::InvalidRequest("invalid proxy URL".into());
317 let base = ureq::Proxy::new(url).map_err(|_| invalid())?;
318 if !matches!(
319 base.protocol(),
320 ureq::ProxyProtocol::Http | ureq::ProxyProtocol::Https
321 ) {
322 return Err(invalid());
323 }
324 let mut builder = ureq::Proxy::builder(base.protocol())
325 .host(base.host())
326 .port(base.port())
327 .resolve_target(false);
328 if let Some(user) = base.username() {
329 builder = builder.username(user);
330 }
331 if let Some(pass) = base.password() {
332 builder = builder.password(pass);
333 }
334 for expr in no_proxy {
335 builder = builder.no_proxy(expr);
336 }
337 let ureq = builder.build().map_err(|_| invalid())?;
338 Ok(ResolvedProxy {
339 host: normalize_host(ureq.host()),
340 port: ureq.port(),
341 ureq,
342 url: url.to_string(),
343 no_proxy: no_proxy.to_vec(),
344 from_env: false,
345 })
346}
347
348pub(crate) enum Routing {
352 Fixed(Option<ResolvedProxy>),
353 PerRequest(Arc<SystemRoutes>),
354}
355
356pub(crate) struct SystemRoutes {
360 settings: crate::system_proxy::SystemProxySettings,
361 http: Option<ResolvedProxy>,
362 https: Option<ResolvedProxy>,
363}
364
365impl SystemRoutes {
366 pub(crate) fn new(
369 settings: &crate::system_proxy::SystemProxySettings,
370 ) -> Result<Self, HttpError> {
371 let raw = settings.proxy.as_deref().unwrap_or_default().trim();
372 let (http, https) = crate::system_proxy::endpoints_by_scheme(raw);
373 let build = |url: Option<String>| -> Result<Option<ResolvedProxy>, HttpError> {
374 url.map(|url| {
375 build_proxy(&url, &[]).map(|mut proxy| {
376 proxy.from_env = true;
377 proxy
378 })
379 })
380 .transpose()
381 };
382 Ok(Self {
383 settings: settings.clone(),
384 http: build(http)?,
385 https: build(https)?,
386 })
387 }
388
389 pub(crate) fn route(&self, scheme: &str, host: &str) -> crate::system_proxy::UrlRoute {
391 crate::system_proxy::static_route_for_url(&self.settings, scheme, host)
392 .unwrap_or(crate::system_proxy::UrlRoute::Direct)
393 }
394
395 pub(crate) fn proxy_for(&self, scheme: &str, host: &str) -> Option<&ResolvedProxy> {
397 match self.route(scheme, host) {
398 crate::system_proxy::UrlRoute::Direct => None,
399 crate::system_proxy::UrlRoute::Proxy(_) => {
400 if scheme == "https" {
401 self.https.as_ref()
402 } else {
403 self.http.as_ref()
404 }
405 }
406 }
407 }
408
409 pub(crate) fn endpoints(&self) -> Vec<(String, u16)> {
411 [&self.http, &self.https]
412 .into_iter()
413 .flatten()
414 .map(|p| (p.host.clone(), p.port))
415 .collect()
416 }
417
418 #[cfg(feature = "async")]
420 pub(crate) fn http(&self) -> Option<&ResolvedProxy> {
421 self.http.as_ref()
422 }
423
424 #[cfg(feature = "async")]
425 pub(crate) fn https(&self) -> Option<&ResolvedProxy> {
426 self.https.as_ref()
427 }
428}
429
430pub(crate) fn uri_scheme_and_host(uri: &ureq::http::Uri) -> (String, String) {
433 (
434 uri.scheme_str().unwrap_or("").to_ascii_lowercase(),
435 normalize_host(uri.host().unwrap_or("")),
436 )
437}
438
439pub(crate) fn proxy_from_lookup(
441 get: &dyn Fn(&str) -> Option<String>,
442) -> Result<Option<ResolvedProxy>, HttpError> {
443 let no_proxy: Vec<String> = ["NO_PROXY", "no_proxy"]
444 .iter()
445 .find_map(|name| get(name).filter(|v| !v.is_empty()))
446 .map(|v| {
447 v.split(',')
448 .map(|e| e.trim().to_string())
449 .filter(|e| !e.is_empty())
450 .collect()
451 })
452 .unwrap_or_default();
453 for name in PROXY_ENV {
454 if let Some(url) = get(name).filter(|v| !v.trim().is_empty()) {
455 if let Ok(mut proxy) = build_proxy(url.trim(), &no_proxy) {
456 proxy.from_env = true;
457 return Ok(Some(proxy));
458 }
459 }
460 }
461 Ok(None)
462}
463
464impl ClientConfig {
465 pub(crate) fn resolved_routing_with_mode(&self) -> Result<(Routing, ProxyModeUsed), HttpError> {
469 let bypassed = self.proxy_bypass_first_party && self.address_policy.is_first_party();
470 if matches!(self.proxy, ProxyMode::System) && !bypassed {
471 let env = |name: &str| std::env::var(name).ok();
472 return resolve_system_routing(&crate::system_proxy::system_proxy_settings(), &env);
473 }
474 self.resolved_proxy_with_mode()
475 .map(|(proxy, used)| (Routing::Fixed(proxy), used))
476 }
477
478 #[allow(dead_code)]
480 pub(crate) fn resolved_proxy(&self) -> Result<Option<ResolvedProxy>, HttpError> {
481 self.resolved_proxy_with_mode().map(|(proxy, _)| proxy)
482 }
483
484 pub(crate) fn resolved_proxy_with_mode(
486 &self,
487 ) -> Result<(Option<ResolvedProxy>, ProxyModeUsed), HttpError> {
488 if self.proxy_bypass_first_party && self.address_policy.is_first_party() {
489 return Ok((None, ProxyModeUsed::Direct));
490 }
491 let env = |name: &str| std::env::var(name).ok();
492 match &self.proxy {
493 ProxyMode::None => Ok((None, ProxyModeUsed::Direct)),
494 ProxyMode::Explicit(url) => {
495 build_proxy(url, &[]).map(|p| (Some(p), ProxyModeUsed::Explicit))
496 }
497 ProxyMode::Environment => {
498 let proxy = proxy_from_lookup(&env)?;
499 let used = if proxy.is_some() {
500 ProxyModeUsed::Environment
501 } else {
502 ProxyModeUsed::Direct
503 };
504 Ok((proxy, used))
505 }
506 ProxyMode::System => {
507 resolve_system(&crate::system_proxy::system_proxy_settings(), &env)
508 }
509 ProxyMode::SystemForUrl(url) => resolve_system_for_url(
510 &crate::system_proxy::system_proxy_settings(),
511 url,
512 &crate::system_proxy::winhttp_route_for_url,
513 &env,
514 ),
515 }
516 }
517}
518
519pub(crate) fn resolve_system_for_url(
522 settings: &crate::system_proxy::SystemProxySettings,
523 url: &str,
524 os: &dyn Fn(
525 &str,
526 &crate::system_proxy::SystemProxySettings,
527 ) -> Option<crate::system_proxy::UrlRoute>,
528 get: &dyn Fn(&str) -> Option<String>,
529) -> Result<(Option<ResolvedProxy>, ProxyModeUsed), HttpError> {
530 use crate::system_proxy::UrlRoute;
531 let Some((scheme, host)) = crate::system_proxy::scheme_and_host(url) else {
532 return Err(HttpError::InvalidRequest(
533 "invalid URL for system proxy resolution".into(),
534 ));
535 };
536 let from_route =
537 |route: UrlRoute| -> Result<(Option<ResolvedProxy>, ProxyModeUsed), HttpError> {
538 match route {
539 UrlRoute::Direct => Ok((None, ProxyModeUsed::SystemForUrl)),
540 UrlRoute::Proxy(proxy_url) => {
541 let mut proxy = build_proxy(&proxy_url, &[])?;
542 proxy.from_env = true;
543 Ok((Some(proxy), ProxyModeUsed::SystemForUrl))
544 }
545 }
546 };
547 let automatic = settings.auto_config || settings.auto_detect;
548 if automatic {
549 if let Some(route) = os(url, settings) {
550 return from_route(route);
551 }
552 if settings.auto_config {
554 return Err(HttpError::SystemProxyUnsupported {
555 element: "the proxy auto-config script could not be evaluated for this URL".into(),
556 });
557 }
558 }
559 if let Some(route) = crate::system_proxy::static_route_for_url(settings, &scheme, &host) {
560 return from_route(route);
561 }
562 let proxy = proxy_from_lookup(get)?;
563 let used = match (settings.auto_detect, proxy.is_some()) {
564 (true, true) => ProxyModeUsed::SystemFallbackEnvironment {
565 reason: AUTO_DETECT_FAILED_REASON.to_string(),
566 },
567 (true, false) => ProxyModeUsed::SystemFallbackDirect {
568 reason: AUTO_DETECT_FAILED_REASON.to_string(),
569 },
570 (false, true) => ProxyModeUsed::Environment,
571 (false, false) => ProxyModeUsed::Direct,
572 };
573 Ok((proxy, used))
574}
575
576pub(crate) const AUTO_DETECT_FAILED_REASON: &str =
578 "automatic detection found no proxy for this URL; no static proxy or PAC configured";
579
580pub(crate) const AUTO_DETECT_ONLY_REASON: &str =
582 "automatic detection only; no static proxy or PAC configured";
583
584pub(crate) fn resolve_system_routing(
588 settings: &crate::system_proxy::SystemProxySettings,
589 get: &dyn Fn(&str) -> Option<String>,
590) -> Result<(Routing, ProxyModeUsed), HttpError> {
591 if matches!(
592 crate::system_proxy::proxy_choice(settings),
593 crate::system_proxy::SystemProxyChoice::PerRequest
594 ) {
595 return Ok((
596 Routing::PerRequest(Arc::new(SystemRoutes::new(settings)?)),
597 ProxyModeUsed::System,
598 ));
599 }
600 resolve_system(settings, get).map(|(proxy, used)| (Routing::Fixed(proxy), used))
601}
602
603pub(crate) fn resolve_system(
613 settings: &crate::system_proxy::SystemProxySettings,
614 get: &dyn Fn(&str) -> Option<String>,
615) -> Result<(Option<ResolvedProxy>, ProxyModeUsed), HttpError> {
616 let env_or_direct = |used_env: ProxyModeUsed,
617 used_direct: ProxyModeUsed|
618 -> Result<(Option<ResolvedProxy>, ProxyModeUsed), HttpError> {
619 let proxy = proxy_from_lookup(get)?;
620 let used = if proxy.is_some() {
621 used_env
622 } else {
623 used_direct
624 };
625 Ok((proxy, used))
626 };
627 if matches!(
628 crate::system_proxy::proxy_choice(settings),
629 crate::system_proxy::SystemProxyChoice::AutoDetectOnly
630 ) {
631 let reason = AUTO_DETECT_ONLY_REASON.to_string();
632 return env_or_direct(
633 ProxyModeUsed::SystemFallbackEnvironment {
634 reason: reason.clone(),
635 },
636 ProxyModeUsed::SystemFallbackDirect { reason },
637 );
638 }
639 match proxy_from_system(settings)? {
640 Some(proxy) => Ok((Some(proxy), ProxyModeUsed::System)),
641 None => env_or_direct(ProxyModeUsed::Environment, ProxyModeUsed::Direct),
642 }
643}
644
645pub(crate) fn proxy_from_system(
655 settings: &crate::system_proxy::SystemProxySettings,
656) -> Result<Option<ResolvedProxy>, HttpError> {
657 use crate::system_proxy::SystemProxyChoice;
658 match crate::system_proxy::proxy_choice(settings) {
659 SystemProxyChoice::None | SystemProxyChoice::AutoDetectOnly => Ok(None),
660 SystemProxyChoice::Single { url, no_proxy } => {
661 Ok(build_proxy(&url, &no_proxy).ok().map(|mut proxy| {
662 proxy.from_env = true;
663 proxy
664 }))
665 }
666 SystemProxyChoice::PerRequest => Err(HttpError::SystemProxyUnsupported {
668 element: "the proxy settings differ by scheme or host and need per-request routing"
669 .into(),
670 }),
671 SystemProxyChoice::Unsupported(element) => {
672 Err(HttpError::SystemProxyUnsupported { element })
673 }
674 }
675}
676
677#[derive(Clone)]
678pub struct ClientConfig {
679 pub user_agent: String,
680 pub connect_timeout: Option<Duration>,
682 pub request_timeout: Duration,
684 pub response_timeout: Option<Duration>,
687 pub recv_body_timeout: Option<Duration>,
690 pub stream_timeout: Option<Duration>,
692 pub idle_timeout: Option<Duration>,
700 pub max_redirects: u32,
702 pub max_redirects_will_error: bool,
706 pub max_response_bytes: usize,
707 pub retry: RetryPolicy,
708 pub on_retry: Option<RetryObserver>,
709 pub on_attempt: Option<AttemptHook>,
715 pub redact_values: Vec<String>,
718 pub address_policy: AddressPolicy,
722 pub proxy: ProxyMode,
725 pub proxy_bypass_first_party: bool,
728 pub trust: TrustRoots,
732 pub https_only: bool,
737}
738
739impl Default for ClientConfig {
740 fn default() -> Self {
741 Self {
742 user_agent: concat!("rightkit-http/", env!("CARGO_PKG_VERSION")).to_string(),
743 connect_timeout: Some(Duration::from_secs(10)),
744 request_timeout: Duration::from_secs(60),
745 response_timeout: Some(Duration::from_secs(60)),
746 recv_body_timeout: None,
747 stream_timeout: None,
748 idle_timeout: None,
749 max_redirects: 0,
750 max_redirects_will_error: true,
751 max_response_bytes: 32 * 1024 * 1024,
752 retry: RetryPolicy::default(),
753 on_retry: None,
754 on_attempt: None,
755 redact_values: Vec::new(),
756 address_policy: AddressPolicy::default(),
757 proxy: ProxyMode::None,
758 proxy_bypass_first_party: false,
759 trust: TrustRoots::Platform,
760 https_only: false,
761 }
762 }
763}
764
765impl ClientConfig {
766 pub fn first_party(hosts: Vec<String>) -> Self {
770 Self::default().address_policy(AddressPolicy::first_party(hosts))
771 }
772
773 pub fn legacy_ureq_parity(user_agent: impl Into<String>) -> Self {
779 Self {
780 user_agent: user_agent.into(),
781 proxy: ProxyMode::Environment,
782 trust: TrustRoots::Bundled,
783 max_redirects: 10,
784 retry: RetryPolicy::none(),
785 ..Self::default()
786 }
787 }
788
789 pub fn on_attempt(
791 mut self,
792 hook: impl Fn(&AttemptEvent) -> Result<(), String> + Send + Sync + 'static,
793 ) -> Self {
794 self.on_attempt = Some(Arc::new(hook));
795 self
796 }
797
798 pub fn address_policy(mut self, policy: AddressPolicy) -> Self {
800 self.address_policy = policy;
801 self
802 }
803
804 pub fn proxy(mut self, mode: ProxyMode) -> Self {
806 self.proxy = mode;
807 self
808 }
809
810 pub fn trust(mut self, trust: TrustRoots) -> Self {
812 self.trust = trust;
813 self
814 }
815
816 pub fn idle_timeout(mut self, idle: Option<Duration>) -> Self {
819 self.idle_timeout = idle;
820 self
821 }
822
823 pub fn https_only(mut self, https_only: bool) -> Self {
825 self.https_only = https_only;
826 self
827 }
828
829 pub fn proxy_bypass_first_party(mut self, bypass: bool) -> Self {
832 self.proxy_bypass_first_party = bypass;
833 self
834 }
835
836 pub fn redact_values(mut self, values: Vec<String>) -> Self {
838 self.redact_values
839 .extend(values.into_iter().filter(|v| !v.is_empty()));
840 self
841 }
842}
843
844#[derive(Clone)]
845pub struct Request {
846 pub method: Method,
847 pub url: String,
848 pub headers: Vec<(String, String)>,
849 pub body: Vec<u8>,
850 pub timeout: Option<Duration>,
852 pub retry: Option<RetryPolicy>,
853 pub retry_unsafe_statuses: bool,
859 pub(crate) max_response_bytes: Option<usize>,
860 pub(crate) deadline: Option<Duration>,
861 pub(crate) cancel: Option<CancelToken>,
862 pub(crate) secret_values: Vec<String>,
863 pub(crate) headers_only: bool,
864}
865
866impl fmt::Debug for Request {
867 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
868 let headers: Vec<(String, String)> = self
869 .headers
870 .iter()
871 .map(|(k, v)| {
872 let shown = if redirect::is_secret_header_name(k) {
873 REDACTED.to_string()
874 } else {
875 redact_str(&redact_urls_in_text(v), &sorted_values(&self.secret_values))
876 };
877 (k.clone(), shown)
878 })
879 .collect();
880 f.debug_struct("Request")
881 .field("method", &self.method)
882 .field(
883 "url",
884 &redact_str(&redact_url(&self.url), &self.secret_values),
885 )
886 .field("headers", &headers)
887 .field("body_len", &self.body.len())
888 .field("timeout", &self.timeout)
889 .field("retry", &self.retry)
890 .field("retry_unsafe_statuses", &self.retry_unsafe_statuses)
891 .field("max_response_bytes", &self.max_response_bytes)
892 .field("deadline", &self.deadline)
893 .field("cancel", &self.cancel.is_some())
894 .field("headers_only", &self.headers_only)
895 .finish()
896 }
897}
898
899impl Request {
900 pub fn new(method: Method, url: impl Into<String>) -> Self {
901 Self {
902 method,
903 url: url.into(),
904 headers: Vec::new(),
905 body: Vec::new(),
906 timeout: None,
907 retry: None,
908 retry_unsafe_statuses: false,
909 max_response_bytes: None,
910 deadline: None,
911 cancel: None,
912 secret_values: Vec::new(),
913 headers_only: false,
914 }
915 }
916 pub fn get(url: impl Into<String>) -> Self {
917 Self::new(Method::Get, url)
918 }
919 pub fn post(url: impl Into<String>) -> Self {
920 Self::new(Method::Post, url)
921 }
922 pub fn put(url: impl Into<String>) -> Self {
923 Self::new(Method::Put, url)
924 }
925 pub fn delete(url: impl Into<String>) -> Self {
926 Self::new(Method::Delete, url)
927 }
928 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
930 self.headers.push((name.into(), value.into()));
931 self
932 }
933 pub fn set_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
936 let name = name.into();
937 let value = value.into();
938 let position = self
939 .headers
940 .iter()
941 .position(|(k, _)| k.eq_ignore_ascii_case(&name));
942 self.headers.retain(|(k, _)| !k.eq_ignore_ascii_case(&name));
943 match position {
944 Some(i) => self.headers.insert(i, (name, value)),
945 None => self.headers.push((name, value)),
946 }
947 self
948 }
949 pub fn secret_header(self, name: impl Into<String>, value: impl Into<String>) -> Self {
952 let value = value.into();
953 let mut req = self;
954 if !value.is_empty() {
955 req.secret_values.push(value.clone());
956 }
957 req.set_header(name, value)
958 }
959 pub fn bearer(mut self, token: &str) -> Self {
961 if !token.is_empty() {
962 self.secret_values.push(token.to_string());
963 }
964 self.header("Authorization", format!("Bearer {token}"))
965 }
966 pub fn redact_value(mut self, value: impl Into<String>) -> Self {
970 let value = value.into();
971 if !value.is_empty() {
972 self.secret_values.push(value);
973 }
974 self
975 }
976 pub fn secret_form(mut self, pairs: &[(&str, &str)], secret_names: &[&str]) -> Self {
980 for (name, value) in pairs {
981 if secret_names.iter().any(|s| s.eq_ignore_ascii_case(name)) && !value.is_empty() {
982 self.secret_values.push((*value).to_string());
983 }
984 }
985 self.form(pairs)
986 }
987 pub fn scrub(&self, text: &str) -> String {
991 redact_str(text, &sorted_values(&self.secret_values))
992 }
993 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
994 self.body = body.into();
995 self
996 }
997 pub fn json<T: Serialize>(mut self, value: &T) -> Result<Self, HttpError> {
998 self.body = serde_json::to_vec(value).map_err(json_error)?;
999 self.headers
1000 .push(("Content-Type".into(), "application/json".into()));
1001 Ok(self)
1002 }
1003 pub fn form(mut self, pairs: &[(&str, &str)]) -> Self {
1005 self.body = form_encode(pairs).into_bytes();
1006 self.headers.push((
1007 "Content-Type".into(),
1008 "application/x-www-form-urlencoded".into(),
1009 ));
1010 self
1011 }
1012 pub fn timeout(mut self, d: Duration) -> Self {
1015 self.timeout = Some(d);
1016 self
1017 }
1018 pub fn deadline(mut self, total: Duration) -> Self {
1022 self.deadline = Some(total);
1023 self
1024 }
1025 pub fn retry(mut self, policy: RetryPolicy) -> Self {
1026 self.retry = Some(policy);
1027 self
1028 }
1029 pub fn retry_unsafe_statuses(mut self, allow: bool) -> Self {
1031 self.retry_unsafe_statuses = allow;
1032 self
1033 }
1034 pub fn max_response_bytes(mut self, limit: usize) -> Self {
1036 self.max_response_bytes = Some(limit);
1037 self
1038 }
1039 pub fn headers_only(mut self) -> Self {
1044 self.headers_only = true;
1045 self
1046 }
1047 pub fn cancel(mut self, token: CancelToken) -> Self {
1050 self.cancel = Some(token);
1051 self
1052 }
1053}
1054
1055pub fn percent_encode(input: &str) -> String {
1057 let mut out = String::with_capacity(input.len());
1058 for b in input.bytes() {
1059 match b {
1060 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1061 out.push(b as char)
1062 }
1063 b' ' => out.push('+'),
1064 _ => out.push_str(&format!("%{b:02X}")),
1065 }
1066 }
1067 out
1068}
1069
1070pub fn percent_decode(input: &str) -> String {
1071 let bytes = input.as_bytes();
1072 let mut out = Vec::with_capacity(bytes.len());
1073 let mut i = 0;
1074 while i < bytes.len() {
1075 match bytes[i] {
1076 b'+' => out.push(b' '),
1077 b'%' if i + 2 < bytes.len() => {
1078 let hi = (bytes[i + 1] as char).to_digit(16);
1079 let lo = (bytes[i + 2] as char).to_digit(16);
1080 match (hi, lo) {
1081 (Some(h), Some(l)) => {
1082 out.push((h * 16 + l) as u8);
1083 i += 2;
1084 }
1085 _ => out.push(b'%'),
1086 }
1087 }
1088 b => out.push(b),
1089 }
1090 i += 1;
1091 }
1092 String::from_utf8_lossy(&out).into_owned()
1093}
1094
1095pub fn percent_encode_component(input: &str) -> String {
1099 let mut out = String::with_capacity(input.len());
1100 for b in input.bytes() {
1101 match b {
1102 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1103 out.push(b as char)
1104 }
1105 _ => out.push_str(&format!("%{b:02X}")),
1106 }
1107 }
1108 out
1109}
1110
1111pub fn percent_decode_component(input: &str) -> String {
1114 let bytes = input.as_bytes();
1115 let mut out = Vec::with_capacity(bytes.len());
1116 let mut i = 0;
1117 while i < bytes.len() {
1118 if bytes[i] == b'%' && i + 2 < bytes.len() {
1119 let hi = (bytes[i + 1] as char).to_digit(16);
1120 let lo = (bytes[i + 2] as char).to_digit(16);
1121 if let (Some(h), Some(l)) = (hi, lo) {
1122 out.push((h * 16 + l) as u8);
1123 i += 3;
1124 continue;
1125 }
1126 }
1127 out.push(bytes[i]);
1128 i += 1;
1129 }
1130 String::from_utf8_lossy(&out).into_owned()
1131}
1132
1133pub fn form_encode(pairs: &[(&str, &str)]) -> String {
1134 pairs
1135 .iter()
1136 .map(|(k, v)| format!("{}={}", percent_encode(k), percent_encode(v)))
1137 .collect::<Vec<_>>()
1138 .join("&")
1139}
1140
1141#[derive(Clone)]
1142pub struct Response {
1143 pub status: u16,
1144 pub headers: Vec<(String, String)>,
1146 pub body: Vec<u8>,
1147 pub attempts: u32,
1148 pub(crate) final_url: String,
1150 pub(crate) redact: Vec<String>,
1153}
1154
1155impl Response {
1156 pub fn header(&self, name: &str) -> Option<&str> {
1157 header_of(&self.headers, name)
1158 }
1159 pub fn header_all<'a>(&'a self, name: &str) -> impl Iterator<Item = &'a str> + 'a {
1162 header_all_of(&self.headers, name)
1163 }
1164 pub fn final_url(&self) -> &str {
1167 &self.final_url
1168 }
1169 pub fn retry_after(&self) -> Option<Duration> {
1171 self.header("retry-after").and_then(parse_retry_after)
1172 }
1173 pub fn text(&self) -> String {
1174 String::from_utf8_lossy(&self.body).into_owned()
1175 }
1176 pub fn json<T: DeserializeOwned>(&self) -> Result<T, HttpError> {
1179 serde_json::from_slice(&self.body).map_err(json_error)
1180 }
1181}
1182
1183pub(crate) fn json_error(e: serde_json::Error) -> HttpError {
1185 HttpError::Json(format!(
1186 "{:?} error at line {} column {}",
1187 e.classify(),
1188 e.line(),
1189 e.column()
1190 ))
1191}
1192
1193impl fmt::Debug for Response {
1194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1195 f.debug_struct("Response")
1196 .field("status", &self.status)
1197 .field("headers", &redact_headers_with(&self.headers, &self.redact))
1198 .field("body_len", &self.body.len())
1199 .field("attempts", &self.attempts)
1200 .finish()
1201 }
1202}
1203
1204#[derive(Clone)]
1207pub struct ResponseHead {
1208 pub status: u16,
1209 pub headers: Vec<(String, String)>,
1211 pub final_url: String,
1214 pub hops: u32,
1216 pub attempts: u32,
1218 pub(crate) redact: Vec<String>,
1220}
1221
1222impl ResponseHead {
1223 pub fn header(&self, name: &str) -> Option<&str> {
1224 header_of(&self.headers, name)
1225 }
1226 pub fn header_all<'a>(&'a self, name: &str) -> impl Iterator<Item = &'a str> + 'a {
1228 header_all_of(&self.headers, name)
1229 }
1230}
1231
1232impl fmt::Debug for ResponseHead {
1233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1234 f.debug_struct("ResponseHead")
1235 .field("status", &self.status)
1236 .field("headers", &redact_headers_with(&self.headers, &self.redact))
1237 .field(
1238 "final_url",
1239 &redact_str(&redact_url(&self.final_url), &self.redact),
1240 )
1241 .field("hops", &self.hops)
1242 .field("attempts", &self.attempts)
1243 .finish()
1244 }
1245}
1246
1247pub struct StreamResponse {
1248 pub status: u16,
1249 pub headers: Vec<(String, String)>,
1250 pub attempts: u32,
1251 reader: Box<dyn Read + Send>,
1252}
1253
1254impl StreamResponse {
1255 pub fn header(&self, name: &str) -> Option<&str> {
1256 header_of(&self.headers, name)
1257 }
1258 pub fn header_all<'a>(&'a self, name: &str) -> impl Iterator<Item = &'a str> + 'a {
1260 header_all_of(&self.headers, name)
1261 }
1262 pub fn copy_to<W: io::Write>(
1266 self,
1267 writer: &mut W,
1268 max_bytes: u64,
1269 mut progress: impl FnMut(u64),
1270 ) -> Result<u64, HttpError> {
1271 let mut reader = self.reader;
1272 let mut buf = vec![0u8; 64 * 1024];
1273 let mut total: u64 = 0;
1274 loop {
1275 let n = match reader.read(&mut buf) {
1276 Ok(0) => break,
1277 Ok(n) => n,
1278 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
1279 Err(e) if is_cancelled_io(&e) => return Err(HttpError::Cancelled),
1280 Err(e) if e.kind() == io::ErrorKind::TimedOut => {
1281 return Err(HttpError::timeout(e.to_string(), Phase::Body))
1282 }
1283 Err(e) => return Err(HttpError::transport(e.to_string(), Phase::Body)),
1284 };
1285 total = total.saturating_add(n as u64);
1286 if total > max_bytes {
1287 return Err(HttpError::BodyTooLarge {
1288 limit: max_bytes as usize,
1289 });
1290 }
1291 writer
1292 .write_all(&buf[..n])
1293 .map_err(|e| HttpError::Stream(format!("write failed: {e}")))?;
1294 progress(total);
1295 }
1296 Ok(total)
1297 }
1298 pub fn into_reader(self) -> Box<dyn Read + Send> {
1299 self.reader
1300 }
1301 pub fn sse(self) -> SseReader<Box<dyn Read + Send>> {
1303 SseReader::new(self.reader)
1304 }
1305}
1306
1307pub(crate) fn header_of<'a>(headers: &'a [(String, String)], name: &str) -> Option<&'a str> {
1308 headers
1309 .iter()
1310 .find(|(k, _)| k.eq_ignore_ascii_case(name))
1311 .map(|(_, v)| v.as_str())
1312}
1313
1314pub(crate) fn header_all_of<'a>(
1316 headers: &'a [(String, String)],
1317 name: &str,
1318) -> impl Iterator<Item = &'a str> + 'a {
1319 let name = name.to_string();
1320 headers
1321 .iter()
1322 .filter(move |(k, _)| k.eq_ignore_ascii_case(&name))
1323 .map(|(_, v)| v.as_str())
1324}
1325
1326#[derive(Clone)]
1327pub struct Client {
1328 agent: ureq::Agent,
1329 config: ClientConfig,
1330 proxy: Option<ureq::Proxy>,
1332 routes: Option<Arc<SystemRoutes>>,
1334 proxy_diagnostic: Option<ProxyDiagnostic>,
1335 proxy_mode_used: ProxyModeUsed,
1337 config_error: Option<String>,
1339 compression: bool,
1341 system_proxy_unsupported: Option<String>,
1345}
1346
1347pub(crate) struct Raw {
1348 pub(crate) status: u16,
1349 pub(crate) headers: Vec<(String, String)>,
1350 pub(crate) body: ureq::Body,
1351 pub(crate) attempts: u32,
1352 pub(crate) final_url: String,
1354 pub(crate) hops: u32,
1356}
1357
1358impl Client {
1359 pub fn new(config: ClientConfig) -> Self {
1362 match Self::try_new(config.clone()) {
1363 Ok(client) => client,
1364 Err(e) => {
1365 let mut direct = config;
1366 direct.proxy = ProxyMode::None;
1367 let mut client = Self::build(direct, Routing::Fixed(None), ProxyModeUsed::Direct);
1368 if let HttpError::SystemProxyUnsupported { element } = &e {
1369 client.system_proxy_unsupported = Some(element.clone());
1370 }
1371 client.config_error = Some(e.to_string());
1372 client
1373 }
1374 }
1375 }
1376
1377 pub fn try_new(config: ClientConfig) -> Result<Self, HttpError> {
1379 let (routing, used) = config.resolved_routing_with_mode()?;
1380 Ok(Self::build(config, routing, used))
1381 }
1382
1383 fn build(config: ClientConfig, routing: Routing, used: ProxyModeUsed) -> Self {
1384 let (proxy, routes) = match routing {
1385 Routing::Fixed(proxy) => (proxy, None),
1386 Routing::PerRequest(routes) => (None, Some(routes)),
1387 };
1388 let builder = ureq::Agent::config_builder()
1391 .http_status_as_error(false)
1392 .max_redirects(0)
1393 .user_agent(config.user_agent.clone())
1394 .timeout_connect(config.connect_timeout)
1395 .timeout_resolve(config.connect_timeout)
1396 .tls_config(blocking_tls_config(config.trust));
1397 let diagnostic = proxy.as_ref().map(ProxyDiagnostic::of);
1398 let (builder, endpoint, proxy) = match proxy {
1399 Some(p) => (
1400 builder.proxy(Some(p.ureq.clone())),
1401 Some((p.host, p.port)),
1402 Some(p.ureq),
1403 ),
1404 None => (builder.proxy(None), None, None),
1405 };
1406 let mut proxies: Vec<(String, u16)> = endpoint.into_iter().collect();
1408 if let Some(routes) = &routes {
1409 proxies.extend(routes.endpoints());
1410 }
1411 let agent = ureq::Agent::with_parts(
1415 builder.build(),
1416 ureq::unversioned::transport::Connector::chain(
1417 ureq::unversioned::transport::Connector::chain(
1418 ConnectStart,
1419 ureq::unversioned::transport::DefaultConnector::default(),
1420 ),
1421 ConnectDone,
1422 ),
1423 VettingResolver {
1424 policy: Arc::new(config.address_policy.clone()),
1425 proxies,
1426 lookup_limit: config.connect_timeout.unwrap_or(Duration::from_secs(10)),
1427 },
1428 );
1429 Self {
1430 agent,
1431 config,
1432 proxy,
1433 routes,
1434 proxy_diagnostic: diagnostic,
1435 proxy_mode_used: used,
1436 config_error: None,
1437 compression: true,
1438 system_proxy_unsupported: None,
1439 }
1440 }
1441
1442 pub fn compression(mut self, on: bool) -> Self {
1447 self.compression = on;
1448 self
1449 }
1450
1451 pub fn verifier_kind(&self) -> &'static str {
1454 self.config.trust.verifier_kind()
1455 }
1456
1457 pub fn proxy_diagnostic(&self) -> Option<&ProxyDiagnostic> {
1461 self.proxy_diagnostic.as_ref()
1462 }
1463
1464 pub fn proxy_mode_used(&self) -> &ProxyModeUsed {
1467 &self.proxy_mode_used
1468 }
1469
1470 pub fn first_party(hosts: Vec<String>) -> Self {
1473 Self::new(ClientConfig::first_party(hosts))
1474 }
1475
1476 pub fn config(&self) -> &ClientConfig {
1477 &self.config
1478 }
1479
1480 pub fn send(&self, req: &Request) -> Result<Response, HttpError> {
1482 self.send_checked(req, None)
1483 }
1484
1485 pub fn send_with_flag(&self, req: &Request, flag: &AtomicBool) -> Result<Response, HttpError> {
1493 self.send_checked(req, Some(flag))
1494 }
1495
1496 pub fn send_any_with_flag(
1498 &self,
1499 req: &Request,
1500 flag: &AtomicBool,
1501 ) -> Result<Response, HttpError> {
1502 self.send_any_checked(req, Some(flag))
1503 }
1504
1505 fn send_checked(
1506 &self,
1507 req: &Request,
1508 flag: Option<&AtomicBool>,
1509 ) -> Result<Response, HttpError> {
1510 let resp = self.send_any_checked(req, flag)?;
1511 if (200..300).contains(&resp.status) {
1512 Ok(resp)
1513 } else {
1514 let values = self.redaction_values(req);
1515 Err(scrub_error(
1516 status_error(resp.status, &resp.headers, resp.body, resp.attempts),
1517 &values,
1518 ))
1519 }
1520 }
1521
1522 pub fn send_any(&self, req: &Request) -> Result<Response, HttpError> {
1524 self.send_any_checked(req, None)
1525 }
1526
1527 fn send_any_checked(
1528 &self,
1529 req: &Request,
1530 flag: Option<&AtomicBool>,
1531 ) -> Result<Response, HttpError> {
1532 let timeout = req.timeout.unwrap_or(self.config.request_timeout);
1533 let cancel = flag.or_else(|| request_cancel(req));
1534 let mut raw = self.execute(req, Some(timeout), cancel)?;
1535 let values = self.redaction_values(req);
1536 if req.headers_only {
1537 return Ok(Response {
1539 status: raw.status,
1540 headers: raw.headers,
1541 body: Vec::new(),
1542 attempts: raw.attempts,
1543 final_url: raw.final_url,
1544 redact: values,
1545 });
1546 }
1547 let limit = req
1548 .max_response_bytes
1549 .unwrap_or(self.config.max_response_bytes);
1550 let mut body_reader = raw.body.as_reader();
1551 let mut reader = CheckedReader {
1552 inner: &mut body_reader,
1553 cancel,
1554 };
1555 let body = read_limited(&mut reader, limit).map_err(|e| scrub_error(e, &values))?;
1556 Ok(Response {
1557 status: raw.status,
1558 headers: raw.headers,
1559 body,
1560 attempts: raw.attempts,
1561 final_url: raw.final_url,
1562 redact: values,
1563 })
1564 }
1565
1566 pub fn send_headers(&self, req: &Request) -> Result<ResponseHead, HttpError> {
1571 let timeout = req.timeout.unwrap_or(self.config.request_timeout);
1572 let raw = self.execute(req, Some(timeout), request_cancel(req))?;
1573 let Raw {
1574 status,
1575 headers,
1576 body,
1577 attempts,
1578 final_url,
1579 hops,
1580 ..
1581 } = raw;
1582 drop(body);
1583 Ok(ResponseHead {
1584 status,
1585 headers,
1586 final_url,
1587 hops,
1588 attempts,
1589 redact: self.redaction_values(req),
1590 })
1591 }
1592
1593 pub fn stream_any(&self, req: &Request) -> Result<StreamResponse, HttpError> {
1598 let total = req.timeout.or(self.config.stream_timeout);
1599 let raw = self.execute(req, total, request_cancel(req))?;
1600 let values = self.redaction_values(req);
1601 Ok(self.wrap_stream(raw, req, values))
1602 }
1603
1604 fn wrap_stream(&self, raw: Raw, req: &Request, values: Vec<String>) -> StreamResponse {
1605 let body: Box<dyn Read + Send> = match self.config.idle_timeout {
1606 Some(idle) => Box::new(crate::idle::IdleReader::new(
1607 Box::new(raw.body.into_reader()),
1608 idle,
1609 req.cancel.clone(),
1610 )),
1611 None => Box::new(raw.body.into_reader()),
1612 };
1613 let guarded = GuardReader {
1614 inner: body,
1615 cancel: req.cancel.clone(),
1616 values,
1617 };
1618 StreamResponse {
1619 status: raw.status,
1620 headers: raw.headers,
1621 attempts: raw.attempts,
1622 reader: Box::new(guarded),
1623 }
1624 }
1625
1626 pub fn stream(&self, req: &Request) -> Result<StreamResponse, HttpError> {
1632 let cancel = request_cancel(req);
1633 let total = req.timeout.or(self.config.stream_timeout);
1634 let mut raw = self.execute(req, total, cancel)?;
1635 let values = self.redaction_values(req);
1636 if !(200..300).contains(&raw.status) {
1637 let mut body_reader = raw.body.as_reader();
1638 let mut reader = CheckedReader {
1639 inner: &mut body_reader,
1640 cancel,
1641 };
1642 let body = read_limited(&mut reader, MAX_ERROR_BODY_BYTES).unwrap_or_default();
1643 if is_set(cancel) {
1644 return Err(HttpError::Cancelled);
1645 }
1646 return Err(scrub_error(
1647 status_error(raw.status, &raw.headers, body, raw.attempts),
1648 &values,
1649 ));
1650 }
1651 Ok(self.wrap_stream(raw, req, values))
1652 }
1653
1654 pub fn get_json<T: DeserializeOwned>(&self, url: &str) -> Result<T, HttpError> {
1655 let req = Request::get(url).header("Accept", "application/json");
1656 let values = self.redaction_values(&req);
1657 self.send(&req)?.json().map_err(|e| scrub_error(e, &values))
1658 }
1659
1660 pub(crate) fn redaction_values(&self, req: &Request) -> Vec<String> {
1662 redaction_values_for(&self.config, req)
1663 }
1664
1665 fn execute(
1668 &self,
1669 req: &Request,
1670 total: Option<Duration>,
1671 cancel: Option<&AtomicBool>,
1672 ) -> Result<Raw, HttpError> {
1673 let values = self.redaction_values(req);
1674 self.execute_inner(req, total, cancel, &values)
1675 .map_err(|e| scrub_error(e, &values))
1676 }
1677
1678 fn execute_inner(
1679 &self,
1680 req: &Request,
1681 total: Option<Duration>,
1682 cancel: Option<&AtomicBool>,
1683 values: &[String],
1684 ) -> Result<Raw, HttpError> {
1685 let policy = req
1686 .retry
1687 .clone()
1688 .unwrap_or_else(|| self.config.retry.clone());
1689 let max = policy.max_attempts.max(1);
1690 let deadline = req.deadline.map(|d| Instant::now() + d);
1691 let mut attempt = 0u32;
1692 loop {
1693 if is_set(cancel) {
1694 return Err(HttpError::Cancelled);
1695 }
1696 let attempt_total = match deadline {
1697 None => total,
1698 Some(dl) => {
1699 let remaining = dl.saturating_duration_since(Instant::now());
1700 if remaining.is_zero() {
1701 return Err(HttpError::timeout(
1702 format!("request deadline exceeded after {attempt} attempt(s)"),
1703 Phase::Headers,
1704 ));
1705 }
1706 Some(total.map_or(remaining, |t| t.min(remaining)))
1707 }
1708 };
1709 attempt += 1;
1710 if let Some(hook) = &self.config.on_attempt {
1711 hook(&AttemptEvent {
1712 url: redact_str(&redact_url(&req.url), values),
1713 method: req.method,
1714 attempt,
1715 max_attempts: max,
1716 })
1717 .map_err(|reason| {
1718 HttpError::InvalidRequest(format!("attempt {attempt} refused: {reason}"))
1719 })?;
1720 }
1721 let (reason, delay) = match self.once(req, attempt_total, values) {
1722 Ok(raw) => {
1723 let retryable = policy.is_retryable_status(raw.status)
1724 && (req.method.is_idempotent() || req.retry_unsafe_statuses);
1725 let retry_after =
1726 header_of(&raw.headers, "retry-after").and_then(parse_retry_after);
1727 if !retryable || attempt >= max || policy.defers(retry_after) {
1728 return Ok(Raw {
1729 attempts: attempt,
1730 ..raw
1731 });
1732 }
1733 let delay = policy.delay(attempt, retry_after);
1734 if deadline_passes(deadline, delay) {
1735 return Ok(Raw {
1736 attempts: attempt,
1737 ..raw
1738 });
1739 }
1740 let mut body = raw.body;
1742 let mut reader = body.as_reader();
1743 let mut checked = CheckedReader {
1744 inner: &mut reader,
1745 cancel,
1746 };
1747 let _ = read_limited(&mut checked, MAX_ERROR_BODY_BYTES);
1748 if is_set(cancel) {
1749 return Err(HttpError::Cancelled);
1750 }
1751 (format!("HTTP {}", raw.status), delay)
1752 }
1753 Err(err) => {
1754 let retryable = match &err {
1755 HttpError::Transport { connect_phase, .. } => {
1756 *connect_phase || req.method.is_idempotent()
1757 }
1758 HttpError::Timeout { .. } => req.method.is_idempotent(),
1759 _ => false,
1760 };
1761 if !retryable || attempt >= max {
1762 return Err(err);
1763 }
1764 let delay = policy.delay(attempt, None);
1765 if deadline_passes(deadline, delay) {
1766 return Err(err);
1767 }
1768 (err.to_string(), delay)
1769 }
1770 };
1771 if let Some(obs) = &self.config.on_retry {
1772 obs(&RetryEvent {
1773 url: redact_str(&redact_url(&req.url), values),
1774 retry: attempt,
1775 reason: redact_str(&redact_urls_in_text(&reason), values),
1776 delay,
1777 });
1778 }
1779 let delay = match deadline {
1782 Some(dl) => delay.min(dl.saturating_duration_since(Instant::now())),
1783 None => delay,
1784 };
1785 if !sleep_cancellable(delay, cancel) {
1786 return Err(HttpError::Cancelled);
1787 }
1788 }
1789 }
1790
1791 fn once(
1794 &self,
1795 req: &Request,
1796 total: Option<Duration>,
1797 secrets: &[String],
1798 ) -> Result<Raw, HttpError> {
1799 let started = Instant::now();
1800 let mut method = req.method;
1801 let mut url = req.url.clone();
1802 let mut headers = req.headers.clone();
1803 if !self.compression
1804 && !headers
1805 .iter()
1806 .any(|(k, _)| k.eq_ignore_ascii_case("accept-encoding"))
1807 {
1808 headers.push(("Accept-Encoding".to_string(), "identity".to_string()));
1809 }
1810 let mut body = req.body.clone();
1811 let mut hops = 0u32;
1812 loop {
1813 let remaining = match total {
1814 None => None,
1815 Some(t) => {
1816 let left = t.saturating_sub(started.elapsed());
1817 if left.is_zero() {
1818 let phase = if hops > 0 {
1819 Phase::Redirect
1820 } else {
1821 Phase::Headers
1822 };
1823 return Err(HttpError::timeout("request", phase));
1824 }
1825 Some(left)
1826 }
1827 };
1828 let raw = match self.once_hop(method, &url, &headers, &body, remaining, hops > 0) {
1829 Ok(raw) => raw,
1830 Err(e) if hops > 0 => return Err(e.into_redirect_phase()),
1831 Err(e) => return Err(e),
1832 };
1833 if self.config.max_redirects == 0 || !redirect::is_redirect_status(raw.status) {
1834 return Ok(Raw {
1835 final_url: url,
1836 hops,
1837 ..raw
1838 });
1839 }
1840 let Some(location) = header_of(&raw.headers, "location") else {
1841 return Ok(Raw {
1842 final_url: url,
1843 hops,
1844 ..raw
1845 });
1846 };
1847 let Some(next_method) = redirect::redirect_method(raw.status, method) else {
1848 return Ok(Raw {
1849 final_url: url,
1850 hops,
1851 ..raw
1852 });
1853 };
1854 if hops >= self.config.max_redirects {
1855 if self.config.max_redirects_will_error {
1856 return Err(HttpError::transport("too many redirects", Phase::Redirect));
1857 }
1858 return Ok(Raw {
1859 final_url: url,
1860 hops,
1861 ..raw
1862 });
1863 }
1864 hops += 1;
1865 let next_url = redirect::resolve_location(&url, location).ok_or_else(|| {
1866 HttpError::InvalidRequest("redirect with an unusable Location".into())
1867 })?;
1868 if let (Some(from), Some(to), Some(host)) = (
1869 redirect::scheme_of(&url),
1870 redirect::scheme_of(&next_url),
1871 redirect::host_of(&next_url),
1872 ) {
1873 redirect::require_https(self.config.https_only, &to, &host, true)?;
1876 self.config
1877 .address_policy
1878 .check_redirect_scheme(&from, &to, &host)
1879 .map_err(|d| HttpError::from(d).into_redirect_phase())?;
1880 }
1881 if redirect::origin(&url) != redirect::origin(&next_url) {
1882 redirect::strip_cross_origin(&mut headers, secrets);
1883 }
1884 if next_method != method {
1885 body.clear();
1886 redirect::strip_body_headers(&mut headers);
1887 }
1888 method = next_method;
1889 url = next_url;
1890 }
1892 }
1893
1894 fn once_hop(
1895 &self,
1896 method: Method,
1897 url: &str,
1898 headers: &[(String, String)],
1899 body: &[u8],
1900 total: Option<Duration>,
1901 redirect_hop: bool,
1902 ) -> Result<Raw, HttpError> {
1903 let mut builder = ureq::http::Request::builder()
1904 .method(method.as_str())
1905 .uri(url);
1906 for (k, v) in headers {
1907 builder = builder.header(k.as_str(), v.as_str());
1908 }
1909 let bodiless =
1913 body.is_empty() && !matches!(method, Method::Post | Method::Put | Method::Patch);
1914 if bodiless {
1915 let request = builder
1916 .body(())
1917 .map_err(|_| HttpError::InvalidRequest("request could not be built".into()))?;
1918 self.run_hop(request, total, redirect_hop)
1919 } else {
1920 let request = builder
1921 .body(body.to_vec())
1922 .map_err(|_| HttpError::InvalidRequest("request could not be built".into()))?;
1923 self.run_hop(request, total, redirect_hop)
1924 }
1925 }
1926
1927 fn run_hop<S: ureq::AsSendBody>(
1929 &self,
1930 request: ureq::http::Request<S>,
1931 total: Option<Duration>,
1932 redirect_hop: bool,
1933 ) -> Result<Raw, HttpError> {
1934 if let Some(element) = &self.system_proxy_unsupported {
1935 return Err(HttpError::SystemProxyUnsupported {
1936 element: element.clone(),
1937 });
1938 }
1939 if let Some(reason) = &self.config_error {
1940 return Err(HttpError::InvalidRequest(reason.clone()));
1941 }
1942 redirect::require_https(
1945 self.config.https_only,
1946 request.uri().scheme_str().unwrap_or(""),
1947 request.uri().host().unwrap_or(""),
1948 redirect_hop,
1949 )?;
1950 let (via_proxy, hop_proxy) = self.vet_proxied_destination(request.uri())?;
1951 let _scope = ViaProxy::enter(via_proxy);
1952 let mut configured = self.agent.configure_request(request);
1953 if let Some(proxy) = hop_proxy {
1954 configured = configured.proxy(Some(proxy));
1956 }
1957 let configured = configured
1958 .timeout_global(total)
1959 .timeout_recv_response(self.config.response_timeout)
1960 .timeout_recv_body(self.config.recv_body_timeout)
1961 .build();
1962 CONNECT_STATE.with(|c| c.set(ConnectState::Idle));
1963 let response = self.agent.run(configured).map_err(|err| {
1964 let default = match CONNECT_STATE.with(|c| c.get()) {
1969 ConnectState::Started => Phase::Connect,
1970 _ => Phase::Headers,
1971 };
1972 map_ureq_error_in(err, default)
1973 })?;
1974 let status = response.status().as_u16();
1975 let headers = response
1976 .headers()
1977 .iter()
1978 .map(|(k, v)| {
1979 (
1980 k.as_str().to_ascii_lowercase(),
1981 String::from_utf8_lossy(v.as_bytes()).into_owned(),
1982 )
1983 })
1984 .collect();
1985 Ok(Raw {
1986 status,
1987 headers,
1988 body: response.into_body(),
1989 attempts: 1,
1990 final_url: String::new(),
1991 hops: 0,
1992 })
1993 }
1994}
1995
1996impl Client {
1997 fn vet_proxied_destination(
2003 &self,
2004 uri: &ureq::http::Uri,
2005 ) -> Result<(bool, Option<ureq::Proxy>), HttpError> {
2006 let policy = &self.config.address_policy;
2007 if let Some(routes) = &self.routes {
2008 let (scheme, host) = uri_scheme_and_host(uri);
2009 let Some(proxy) = routes.proxy_for(&scheme, &host) else {
2010 return Ok((false, None));
2013 };
2014 let raw_host = uri.host().unwrap_or("");
2015 policy.admit(&scheme, raw_host)?;
2016 policy.vet_via_proxy(raw_host)?;
2017 return Ok((true, Some(proxy.ureq.clone())));
2018 }
2019 let Some(proxy) = &self.proxy else {
2020 return Ok((false, None));
2021 };
2022 if proxy.is_no_proxy(uri) {
2023 return Ok((false, None));
2024 }
2025 let scheme = uri.scheme_str().unwrap_or("").to_ascii_lowercase();
2026 let raw_host = uri.host().unwrap_or("");
2027 policy.admit(&scheme, raw_host)?;
2028 policy.vet_via_proxy(raw_host)?;
2029 Ok((true, None))
2030 }
2031}
2032
2033fn sorted_values(values: &[String]) -> Vec<String> {
2034 let mut v: Vec<String> = values.iter().filter(|v| !v.is_empty()).cloned().collect();
2035 v.sort_by_key(|s| std::cmp::Reverse(s.len()));
2036 v
2037}
2038
2039pub(crate) fn redact_headers_for_debug(headers: &[(String, String)]) -> Vec<(String, String)> {
2043 headers
2044 .iter()
2045 .map(|(k, v)| {
2046 let shown = if redirect::is_secret_header_name(k) {
2047 REDACTED.to_string()
2048 } else if matches!(
2049 k.to_ascii_lowercase().as_str(),
2050 "location" | "content-location" | "refresh" | "link" | "referer"
2051 ) {
2052 redact_urls_in_text(&redact_url(v))
2053 } else {
2054 redact_urls_in_text(v)
2055 };
2056 (k.clone(), shown)
2057 })
2058 .collect()
2059}
2060
2061pub(crate) fn redact_headers_with(
2063 headers: &[(String, String)],
2064 values: &[String],
2065) -> Vec<(String, String)> {
2066 redact_headers_for_debug(headers)
2067 .into_iter()
2068 .map(|(k, v)| (k, redact_str(&v, values)))
2069 .collect()
2070}
2071
2072pub(crate) fn redaction_values_for(config: &ClientConfig, req: &Request) -> Vec<String> {
2075 let mut values: Vec<String> = config
2076 .redact_values
2077 .iter()
2078 .chain(req.secret_values.iter())
2079 .filter(|v| !v.is_empty())
2080 .cloned()
2081 .chain(url_secret_parts(&req.url))
2082 .collect();
2083 values.sort_by_key(|v| std::cmp::Reverse(v.len()));
2084 values.dedup();
2085 values
2086}
2087
2088fn request_cancel(req: &Request) -> Option<&AtomicBool> {
2089 req.cancel.as_ref().map(CancelToken::as_atomic)
2090}
2091
2092fn deadline_passes(deadline: Option<Instant>, delay: Duration) -> bool {
2093 deadline.is_some_and(|dl| Instant::now() + delay >= dl)
2094}
2095
2096struct GuardReader {
2099 inner: Box<dyn Read + Send>,
2100 cancel: Option<CancelToken>,
2101 values: Vec<String>,
2102}
2103
2104impl Read for GuardReader {
2105 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2106 if self.cancel.as_ref().is_some_and(|c| c.is_cancelled()) {
2107 return Err(crate::cancel::cancelled_io());
2108 }
2109 self.inner.read(buf).map_err(|e| {
2110 if is_cancelled_io(&e) {
2111 return e;
2112 }
2113 let text = e.to_string();
2114 let scrubbed = redact_str(&redact_urls_in_text(&text), &self.values);
2115 if scrubbed == text {
2116 e
2117 } else {
2118 io::Error::new(e.kind(), scrubbed)
2119 }
2120 })
2121 }
2122}
2123
2124#[derive(Clone, Copy, PartialEq, Eq)]
2125enum ConnectState {
2126 Idle,
2128 Started,
2130 Done,
2132}
2133
2134thread_local! {
2135 static CONNECT_STATE: std::cell::Cell<ConnectState> = const { std::cell::Cell::new(ConnectState::Idle) };
2138}
2139
2140#[derive(Debug)]
2142struct ConnectStart;
2143
2144impl ureq::unversioned::transport::Connector<()> for ConnectStart {
2145 type Out = ();
2146 fn connect(
2147 &self,
2148 _details: &ureq::unversioned::transport::ConnectionDetails,
2149 chained: Option<()>,
2150 ) -> Result<Option<()>, ureq::Error> {
2151 CONNECT_STATE.with(|c| c.set(ConnectState::Started));
2152 Ok(chained)
2153 }
2154}
2155
2156#[derive(Debug)]
2158struct ConnectDone;
2159
2160impl ureq::unversioned::transport::Connector<Box<dyn ureq::unversioned::transport::Transport>>
2161 for ConnectDone
2162{
2163 type Out = Box<dyn ureq::unversioned::transport::Transport>;
2164 fn connect(
2165 &self,
2166 _details: &ureq::unversioned::transport::ConnectionDetails,
2167 chained: Option<Self::Out>,
2168 ) -> Result<Option<Self::Out>, ureq::Error> {
2169 CONNECT_STATE.with(|c| c.set(ConnectState::Done));
2170 Ok(chained)
2171 }
2172}
2173
2174pub(crate) fn status_error(
2175 status: u16,
2176 headers: &[(String, String)],
2177 body: Vec<u8>,
2178 attempts: u32,
2179) -> HttpError {
2180 let mut body = body;
2181 body.truncate(MAX_ERROR_BODY_BYTES);
2182 HttpError::Status {
2183 status,
2184 body: String::from_utf8_lossy(&body).into_owned(),
2185 retry_after: header_of(headers, "retry-after").and_then(parse_retry_after),
2186 attempts,
2187 }
2188}
2189
2190pub(crate) fn redact_str(text: &str, values: &[String]) -> String {
2192 let mut out = text.to_string();
2193 for value in values {
2194 if !value.is_empty() && out.contains(value.as_str()) {
2195 out = out.replace(value.as_str(), REDACTED);
2196 }
2197 }
2198 out
2199}
2200
2201pub(crate) fn scrub_error(err: HttpError, values: &[String]) -> HttpError {
2203 let r = |s: String| redact_str(&redact_urls_in_text(&s), values);
2206 match err {
2207 HttpError::InvalidRequest(m) => HttpError::InvalidRequest(r(m)),
2208 HttpError::Transport {
2209 message,
2210 connect_phase,
2211 phase,
2212 } => HttpError::Transport {
2213 message: r(message),
2214 connect_phase,
2215 phase,
2216 },
2217 HttpError::Timeout { message, phase } => HttpError::Timeout {
2218 message: r(message),
2219 phase,
2220 },
2221 HttpError::Status {
2222 status,
2223 body,
2224 retry_after,
2225 attempts,
2226 } => HttpError::Status {
2227 status,
2228 body: redact_str(&body, values),
2229 retry_after,
2230 attempts,
2231 },
2232 HttpError::BodyTooLarge { limit } => HttpError::BodyTooLarge { limit },
2233 HttpError::Json(m) => HttpError::Json(r(m)),
2234 HttpError::Stream(m) => HttpError::Stream(r(m)),
2235 HttpError::Cancelled => HttpError::Cancelled,
2236 HttpError::SystemProxyUnsupported { element } => HttpError::SystemProxyUnsupported {
2237 element: element.clone(),
2238 },
2239 HttpError::HttpsRequired {
2240 host,
2241 scheme,
2242 redirect,
2243 } => HttpError::HttpsRequired {
2244 host: r(host),
2245 scheme,
2246 redirect,
2247 },
2248 HttpError::AddressDenied {
2249 host,
2250 addr,
2251 class,
2252 reason,
2253 } => HttpError::AddressDenied {
2254 host: r(host),
2255 addr,
2256 class,
2257 reason: r(reason),
2258 },
2259 }
2260}
2261
2262pub(crate) fn read_limited(reader: &mut dyn Read, limit: usize) -> Result<Vec<u8>, HttpError> {
2263 let mut out = Vec::new();
2264 reader
2265 .take(limit as u64 + 1)
2266 .read_to_end(&mut out)
2267 .map_err(|e| {
2268 if is_cancelled_io(&e) {
2269 return HttpError::Cancelled;
2270 }
2271 match e.kind() {
2272 std::io::ErrorKind::TimedOut => HttpError::timeout(e.to_string(), Phase::Body),
2273 _ => map_ureq_error_in(e.into(), Phase::Body),
2276 }
2277 })?;
2278 if out.len() > limit {
2279 return Err(HttpError::BodyTooLarge { limit });
2280 }
2281 Ok(out)
2282}
2283
2284fn timeout_phase(t: &ureq::Timeout, default: Phase) -> Phase {
2286 use ureq::Timeout as T;
2287 match t {
2288 T::Resolve | T::Connect => Phase::Connect,
2289 T::SendRequest | T::SendBody | T::RecvResponse => Phase::Headers,
2290 T::RecvBody => Phase::Body,
2291 _ => default,
2292 }
2293}
2294
2295pub(crate) fn map_ureq_error_in(err: ureq::Error, default: Phase) -> HttpError {
2298 use ureq::Error as E;
2299 if let E::Io(io) = &err {
2301 if let Some(inner) = io.get_ref() {
2302 if let Some(denial) = denial_in_chain(inner) {
2303 return denial.into();
2304 }
2305 }
2306 }
2307 match err {
2308 E::Timeout(t) => HttpError::timeout(format!("{t:?}"), timeout_phase(&t, default)),
2309 E::TooManyRedirects => HttpError::transport("too many redirects", Phase::Redirect),
2310 E::HostNotFound => HttpError::transport("host not found", Phase::Connect),
2311 E::ConnectionFailed => HttpError::transport("connection failed", Phase::Connect),
2312 E::Io(io)
2313 if matches!(
2314 io.kind(),
2315 std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::NotFound
2316 ) =>
2317 {
2318 HttpError::transport(io.to_string(), Phase::Connect)
2319 }
2320 E::Io(io) => HttpError::transport(io.to_string(), default),
2321 E::BadUri(m) => HttpError::InvalidRequest(m),
2322 E::Http(e) => HttpError::InvalidRequest(e.to_string()),
2323 other => HttpError::transport(other.to_string(), default),
2324 }
2325}
2326
2327#[cfg(test)]
2328mod tests {
2329 use super::*;
2330
2331 #[test]
2332 fn legacy_ureq_parity_sets_only_the_ureq_fields() {
2333 let cfg = ClientConfig::legacy_ureq_parity("heardright/1.0");
2334 let base = ClientConfig::default();
2335 assert_eq!(cfg.user_agent, "heardright/1.0");
2336 assert!(matches!(cfg.proxy, ProxyMode::Environment));
2337 assert_eq!(cfg.trust, TrustRoots::Bundled);
2338 assert_eq!(cfg.max_redirects, 10);
2339 assert_eq!(cfg.retry, RetryPolicy::none());
2340 assert_eq!(
2342 format!("{:?}", cfg.address_policy),
2343 format!("{:?}", base.address_policy)
2344 );
2345 assert_eq!(cfg.connect_timeout, base.connect_timeout);
2347 assert_eq!(cfg.request_timeout, base.request_timeout);
2348 assert_eq!(cfg.response_timeout, base.response_timeout);
2349 assert_eq!(cfg.recv_body_timeout, base.recv_body_timeout);
2350 assert_eq!(cfg.stream_timeout, base.stream_timeout);
2351 assert_eq!(cfg.idle_timeout, base.idle_timeout);
2352 assert_eq!(cfg.max_redirects_will_error, base.max_redirects_will_error);
2353 assert_eq!(cfg.max_response_bytes, base.max_response_bytes);
2354 assert!(cfg.on_retry.is_none());
2355 assert!(cfg.on_attempt.is_none());
2356 assert!(cfg.redact_values.is_empty());
2357 assert_eq!(cfg.proxy_bypass_first_party, base.proxy_bypass_first_party);
2358 assert_eq!(cfg.https_only, base.https_only);
2359 }
2360
2361 #[test]
2362 fn put_and_delete_set_their_methods() {
2363 assert_eq!(Request::put("http://x/").method, Method::Put);
2364 assert_eq!(Request::delete("http://x/").method, Method::Delete);
2365 assert!(Request::put("http://x/").body.is_empty());
2366 }
2367
2368 #[test]
2369 fn set_header_replaces_case_insensitively_in_place() {
2370 let req = Request::post("http://x/")
2371 .header("X-A", "1")
2372 .header("Accept", "text/plain")
2373 .header("x-a", "2")
2374 .set_header("X-A", "3");
2375 assert_eq!(
2376 req.headers,
2377 vec![
2378 ("X-A".to_string(), "3".to_string()),
2379 ("Accept".to_string(), "text/plain".to_string()),
2380 ]
2381 );
2382 let req = req.set_header("content-type", "text/csv");
2383 assert_eq!(
2384 req.headers.last(),
2385 Some(&("content-type".to_string(), "text/csv".to_string()))
2386 );
2387 }
2388
2389 #[test]
2390 fn redaction_replaces_values_and_longest_first_scrub() {
2391 let values = vec!["secret-token-123".to_string(), "secret".to_string()];
2392 assert_eq!(
2393 redact_str("bad secret-token-123 and secret", &values),
2394 "bad [redacted] and [redacted]"
2395 );
2396 }
2397
2398 #[test]
2399 fn secret_header_hidden_in_debug_but_sent() {
2400 let req = Request::get("http://x/").secret_header("X-Key", "hunter2-value");
2401 assert!(req.headers.iter().any(|(_, v)| v == "hunter2-value"));
2402 let debug = format!("{req:?}");
2403 assert!(!debug.contains("hunter2-value"), "{debug}");
2404 assert!(debug.contains(REDACTED));
2405 }
2406
2407 #[test]
2408 fn scrub_error_covers_text_variants() {
2409 let values = vec!["tok-9".to_string()];
2410 match scrub_error(
2411 HttpError::Status {
2412 status: 401,
2413 body: "invalid tok-9".into(),
2414 retry_after: None,
2415 attempts: 1,
2416 },
2417 &values,
2418 ) {
2419 HttpError::Status { body, .. } => assert_eq!(body, "invalid [redacted]"),
2420 other => panic!("unexpected {other:?}"),
2421 }
2422 }
2423
2424 fn env_of(pairs: &'static [(&'static str, &'static str)]) -> impl Fn(&str) -> Option<String> {
2425 move |name| {
2426 pairs
2427 .iter()
2428 .find(|(k, _)| *k == name)
2429 .map(|(_, v)| v.to_string())
2430 }
2431 }
2432
2433 fn uri(s: &str) -> ureq::http::Uri {
2434 s.parse().unwrap()
2435 }
2436
2437 #[test]
2438 fn environment_proxy_uses_the_first_set_variable_and_skips_empty_ones() {
2439 let env = env_of(&[
2440 ("ALL_PROXY", ""),
2441 ("HTTPS_PROXY", "http://secure.proxy:3128"),
2442 ("HTTP_PROXY", "http://plain.proxy:8080"),
2443 ("NO_PROXY", "a.test, .b.test ,"),
2444 ]);
2445 let p = proxy_from_lookup(&env).unwrap().unwrap();
2446 assert_eq!(p.host, "secure.proxy");
2447 assert_eq!(p.port, 3128);
2448 assert_eq!(
2449 p.no_proxy,
2450 vec!["a.test".to_string(), ".b.test".to_string()]
2451 );
2452 assert!(p.ureq.is_no_proxy(&uri("http://a.test/")));
2453 assert!(p.ureq.is_no_proxy(&uri("http://x.b.test/")));
2454 assert!(!p.ureq.is_no_proxy(&uri("http://c.test/")));
2455 }
2456
2457 #[test]
2458 fn environment_proxy_order_and_case() {
2459 let env = env_of(&[
2460 ("http_proxy", "http://lower.proxy:1"),
2461 ("ALL_PROXY", "http://all.proxy:2"),
2462 ]);
2463 assert_eq!(proxy_from_lookup(&env).unwrap().unwrap().host, "all.proxy");
2464 let env = env_of(&[("http_proxy", "http://lower.proxy:1")]);
2465 assert_eq!(
2466 proxy_from_lookup(&env).unwrap().unwrap().host,
2467 "lower.proxy"
2468 );
2469 let env = env_of(&[("no_proxy", "z.test"), ("https_proxy", "http://p.proxy:9")]);
2470 assert_eq!(
2471 proxy_from_lookup(&env).unwrap().unwrap().no_proxy,
2472 vec!["z.test".to_string()]
2473 );
2474 }
2475
2476 #[test]
2477 fn environment_without_a_usable_proxy_is_none() {
2478 assert!(proxy_from_lookup(&env_of(&[])).unwrap().is_none());
2479 let env = env_of(&[
2481 ("ALL_PROXY", "ftp://nope.proxy:21"),
2482 ("HTTP_PROXY", "http://ok.proxy:80"),
2483 ]);
2484 assert_eq!(proxy_from_lookup(&env).unwrap().unwrap().host, "ok.proxy");
2485 assert!(
2486 proxy_from_lookup(&env_of(&[("ALL_PROXY", "ftp://nope.proxy:21")]))
2487 .unwrap()
2488 .is_none()
2489 );
2490 }
2491
2492 #[test]
2493 fn explicit_proxy_lets_the_proxy_resolve_and_keeps_credentials() {
2494 let p = build_proxy("http://user:pass@Proxy.Corp:3128", &[]).unwrap();
2495 assert_eq!(p.host, "proxy.corp");
2496 assert_eq!(p.port, 3128);
2497 assert!(!p.ureq.resolve_target());
2498 assert_eq!(p.ureq.username(), Some("user"));
2499 assert_eq!(p.ureq.password(), Some("pass"));
2500 assert!(build_proxy("https://10.0.0.5:3128", &[]).is_ok());
2501 assert!(build_proxy("socks5://127.0.0.1:1080", &[]).is_err());
2502 assert!(build_proxy("ftp://x:1", &[]).is_err());
2503 assert!(build_proxy("not a url", &[]).is_err());
2504 }
2505
2506 #[test]
2507 fn proxy_mode_debug_never_shows_credentials() {
2508 let shown = format!(
2509 "{:?}",
2510 ProxyMode::Explicit("http://user:pw-9@proxy:3128/x?k=v".into())
2511 );
2512 assert!(
2513 !shown.contains("pw-9") && !shown.contains("user:") && !shown.contains("k=v"),
2514 "{shown}"
2515 );
2516 assert_eq!(format!("{:?}", ProxyMode::None), "None");
2517 assert_eq!(format!("{:?}", ProxyMode::Environment), "Environment");
2518 assert_eq!(format!("{:?}", ProxyMode::System), "System");
2519 }
2520
2521 #[test]
2522 fn system_settings_resolve_like_an_environment_proxy() {
2523 use crate::system_proxy::SystemProxySettings;
2524 let proxy = proxy_from_system(&SystemProxySettings {
2525 proxy: Some("http=web.corp:8080;https=web.corp:8080".into()),
2526 bypass: Some("*.corp.example;intranet".into()),
2527 ..SystemProxySettings::default()
2528 })
2529 .unwrap()
2530 .expect("a static system proxy resolves");
2531 assert_eq!((proxy.host.as_str(), proxy.port), ("web.corp", 8080));
2532 assert_eq!(
2533 proxy.no_proxy,
2534 vec![".corp.example".to_string(), "intranet".to_string()]
2535 );
2536 assert!(ProxyDiagnostic::of(&proxy).from_environment);
2537 assert!(proxy_from_system(&SystemProxySettings::default())
2539 .unwrap()
2540 .is_none());
2541 }
2542
2543 #[test]
2544 fn system_settings_a_pac_script_fails_closed_and_split_configs_need_per_request_routing() {
2545 use crate::system_proxy::SystemProxySettings;
2546 let pac = SystemProxySettings {
2548 auto_config: true,
2549 ..SystemProxySettings::default()
2550 };
2551 let expected = "a proxy auto-config script is set";
2552 let error = proxy_from_system(&pac)
2553 .err()
2554 .expect("a PAC script is refused");
2555 assert!(
2556 matches!(&error, HttpError::SystemProxyUnsupported { element } if element == expected),
2557 "{error}"
2558 );
2559 assert_eq!(
2560 error.to_string(),
2561 format!(
2562 "the Windows proxy settings cannot be followed exactly by this version \
2563 ({expected}); nothing was sent"
2564 )
2565 );
2566 assert!(matches!(error.class(), crate::error::ErrorClass::Invalid));
2567 for (proxy, bypass) in [
2570 ("http=web.corp:8080;https=secure.corp:8443", None),
2571 ("https=secure.corp:8443", None),
2572 ("web.corp:8080", Some("<local>")),
2573 ("web.corp:8080", Some("https://only.example")),
2574 ] {
2575 let settings = SystemProxySettings {
2576 proxy: Some(proxy.into()),
2577 bypass: bypass.map(str::to_string),
2578 ..SystemProxySettings::default()
2579 };
2580 let no_env = |_: &str| None::<String>;
2581 let (routing, used) = resolve_system_routing(&settings, &no_env).unwrap();
2582 assert!(
2583 matches!(routing, Routing::PerRequest(_)),
2584 "{proxy} {bypass:?}"
2585 );
2586 assert_eq!(used, ProxyModeUsed::System);
2587 }
2588 }
2589
2590 #[test]
2591 fn system_mode_with_automatic_detection_only_goes_direct_or_environment() {
2592 use crate::system_proxy::SystemProxySettings;
2593 let no_env = |_: &str| None::<String>;
2594 let env_proxy =
2595 |name: &str| (name == "HTTPS_PROXY").then(|| "http://env.corp:3128".to_string());
2596 let detect = SystemProxySettings {
2598 auto_detect: true,
2599 ..SystemProxySettings::default()
2600 };
2601 let reason = "automatic detection only; no static proxy or PAC configured";
2602 let (proxy, used) = resolve_system(&detect, &no_env).unwrap();
2603 assert!(proxy.is_none());
2604 assert_eq!(
2605 used,
2606 ProxyModeUsed::SystemFallbackDirect {
2607 reason: reason.to_string()
2608 }
2609 );
2610 let (proxy, used) = resolve_system(&detect, &env_proxy).unwrap();
2611 let proxy = proxy.expect("the environment proxy is used");
2612 assert_eq!((proxy.host.as_str(), proxy.port), ("env.corp", 3128));
2613 assert!(proxy.from_env);
2614 assert_eq!(
2615 used,
2616 ProxyModeUsed::SystemFallbackEnvironment {
2617 reason: reason.to_string()
2618 }
2619 );
2620 let with_bypass = SystemProxySettings {
2622 bypass: Some("<local>".into()),
2623 ..detect
2624 };
2625 let (proxy, used) = resolve_system(&with_bypass, &no_env).unwrap();
2626 assert!(proxy.is_none());
2627 assert!(matches!(used, ProxyModeUsed::SystemFallbackDirect { .. }));
2628 }
2629
2630 #[test]
2631 fn system_mode_with_automatic_detection_and_a_static_proxy_follows_the_proxy() {
2632 use crate::system_proxy::SystemProxySettings;
2633 let no_env = |_: &str| None::<String>;
2634 let env_proxy =
2635 |name: &str| (name == "HTTPS_PROXY").then(|| "http://env.corp:3128".to_string());
2636 let settings = SystemProxySettings {
2637 proxy: Some("web.corp:8080".into()),
2638 bypass: Some("localhost".into()),
2639 auto_detect: true,
2640 ..SystemProxySettings::default()
2641 };
2642 for get in [&no_env as &dyn Fn(&str) -> Option<String>, &env_proxy] {
2643 let (proxy, used) = resolve_system(&settings, get).unwrap();
2644 let proxy = proxy.expect("the static proxy is used");
2645 assert_eq!((proxy.host.as_str(), proxy.port), ("web.corp", 8080));
2646 assert_eq!(used, ProxyModeUsed::System);
2647 }
2648 }
2649
2650 #[test]
2651 fn system_mode_still_refuses_a_pac_script() {
2652 use crate::system_proxy::SystemProxySettings;
2653 let no_env = |_: &str| None::<String>;
2654 let env_proxy =
2655 |name: &str| (name == "HTTPS_PROXY").then(|| "http://env.corp:3128".to_string());
2656 let static_proxy = |proxy: &str, bypass: Option<&str>| SystemProxySettings {
2657 proxy: Some(proxy.into()),
2658 bypass: bypass.map(str::to_string),
2659 ..SystemProxySettings::default()
2660 };
2661 let rows: Vec<(SystemProxySettings, &str)> = vec![
2662 (
2663 SystemProxySettings {
2664 auto_config: true,
2665 ..SystemProxySettings::default()
2666 },
2667 "a proxy auto-config script is set",
2668 ),
2669 (
2670 SystemProxySettings {
2671 auto_config: true,
2672 auto_detect: true,
2673 ..static_proxy("web.corp:8080", None)
2674 },
2675 "a proxy auto-config script is set",
2676 ),
2677 ];
2678 for (settings, reason) in rows {
2681 for get in [&no_env as &dyn Fn(&str) -> Option<String>, &env_proxy] {
2682 let error = resolve_system(&settings, get)
2683 .err()
2684 .unwrap_or_else(|| panic!("{reason}: must be refused"));
2685 assert!(
2686 matches!(&error, HttpError::SystemProxyUnsupported { element } if element == reason),
2687 "{reason}: {error}"
2688 );
2689 }
2690 }
2691 }
2692
2693 #[test]
2694 fn system_mode_reports_what_it_followed() {
2695 use crate::system_proxy::SystemProxySettings;
2696 let no_env = |_: &str| None::<String>;
2697 let env_proxy =
2698 |name: &str| (name == "HTTP_PROXY").then(|| "http://env.corp:3128".to_string());
2699 let followed = SystemProxySettings {
2701 proxy: Some("web.corp:8080".into()),
2702 bypass: Some("localhost;*.corp.example".into()),
2703 ..SystemProxySettings::default()
2704 };
2705 let (proxy, used) = resolve_system(&followed, &no_env).unwrap();
2706 let proxy = proxy.expect("the system proxy is used");
2707 assert_eq!((proxy.host.as_str(), proxy.port), ("web.corp", 8080));
2708 assert_eq!(used, ProxyModeUsed::System);
2709 let (proxy, used) = resolve_system(&followed, &env_proxy).unwrap();
2710 assert_eq!(proxy.expect("the system proxy wins").host, "web.corp");
2711 assert_eq!(used, ProxyModeUsed::System);
2712 let none = SystemProxySettings::default();
2715 let (proxy, used) = resolve_system(&none, &no_env).unwrap();
2716 assert!(proxy.is_none());
2717 assert_eq!(used, ProxyModeUsed::Direct);
2718 let (proxy, used) = resolve_system(&none, &env_proxy).unwrap();
2719 assert!(proxy.is_some());
2720 assert_eq!(used, ProxyModeUsed::Environment);
2721 }
2722
2723 #[test]
2724 fn system_for_url_follows_the_resolver_then_the_static_settings() {
2725 use crate::system_proxy::{SystemProxySettings, UrlRoute};
2726 let no_env = |_: &str| None::<String>;
2727 let url = "https://updates.example.com/feed.json";
2728 let never = |_: &str, _: &SystemProxySettings| None::<UrlRoute>;
2729 let pac_says =
2730 |route: UrlRoute| move |_: &str, _: &SystemProxySettings| Some(route.clone());
2731
2732 let pac = SystemProxySettings {
2734 auto_config: true,
2735 auto_config_url: Some("http://wpad.corp/proxy.pac".into()),
2736 proxy: Some("static.corp:8080".into()),
2737 ..SystemProxySettings::default()
2738 };
2739 let os = pac_says(UrlRoute::Proxy("http://pac.corp:3128".into()));
2740 let (proxy, used) = resolve_system_for_url(&pac, url, &os, &no_env).unwrap();
2741 assert_eq!(proxy.expect("PAC proxy").host, "pac.corp");
2742 assert_eq!(used, ProxyModeUsed::SystemForUrl);
2743 let os = pac_says(UrlRoute::Direct);
2744 let (proxy, used) = resolve_system_for_url(&pac, url, &os, &no_env).unwrap();
2745 assert!(proxy.is_none());
2746 assert_eq!(used, ProxyModeUsed::SystemForUrl);
2747
2748 let err = resolve_system_for_url(&pac, url, &never, &no_env)
2750 .err()
2751 .unwrap();
2752 assert!(
2753 matches!(err, HttpError::SystemProxyUnsupported { .. }),
2754 "{err:?}"
2755 );
2756
2757 let detect = SystemProxySettings {
2759 auto_detect: true,
2760 proxy: Some("http=web.corp:8080;https=secure.corp:8443".into()),
2761 bypass: Some("<local>;*.corp.example;http://plain.example".into()),
2762 ..SystemProxySettings::default()
2763 };
2764 let routed = |target: &str| {
2765 resolve_system_for_url(&detect, target, &never, &no_env)
2766 .unwrap()
2767 .0
2768 .map(|p| p.host)
2769 };
2770 assert_eq!(
2771 routed("https://api.example.com/").as_deref(),
2772 Some("secure.corp")
2773 );
2774 assert_eq!(
2775 routed("http://api.example.com/").as_deref(),
2776 Some("web.corp")
2777 );
2778 assert_eq!(routed("https://intranet/").as_deref(), None);
2779 assert_eq!(routed("https://a.corp.example/").as_deref(), None);
2780 assert_eq!(routed("http://plain.example/").as_deref(), None);
2781 assert_eq!(
2782 routed("https://plain.example/").as_deref(),
2783 Some("secure.corp")
2784 );
2785
2786 let only = SystemProxySettings {
2788 auto_detect: true,
2789 ..SystemProxySettings::default()
2790 };
2791 let (proxy, used) = resolve_system_for_url(&only, url, &never, &no_env).unwrap();
2792 assert!(proxy.is_none());
2793 assert!(matches!(used, ProxyModeUsed::SystemFallbackDirect { .. }));
2794 let env_proxy =
2795 |name: &str| (name == "HTTPS_PROXY").then(|| "http://env.corp:3128".to_string());
2796 let (proxy, used) = resolve_system_for_url(&only, url, &never, &env_proxy).unwrap();
2797 assert_eq!(proxy.expect("environment proxy").host, "env.corp");
2798 assert!(matches!(
2799 used,
2800 ProxyModeUsed::SystemFallbackEnvironment { .. }
2801 ));
2802
2803 for bad in ["not a url", "ftp://host/file", "/relative"] {
2805 let err = resolve_system_for_url(&only, bad, &never, &no_env)
2806 .err()
2807 .unwrap();
2808 assert!(
2809 matches!(err, HttpError::InvalidRequest(_)),
2810 "{bad}: {err:?}"
2811 );
2812 }
2813 }
2814
2815 #[test]
2816 fn system_for_url_debug_hides_credentials() {
2817 let shown = format!(
2818 "{:?}",
2819 ProxyMode::SystemForUrl("https://u:pw-9@host/p?k=v".into())
2820 );
2821 assert!(!shown.contains("pw-9") && !shown.contains("k=v"), "{shown}");
2822 }
2823
2824 #[test]
2825 fn first_party_bypass_drops_the_proxy_only_when_asked() {
2826 let base = ClientConfig::first_party(vec!["api.example.com".into()])
2827 .proxy(ProxyMode::Explicit("http://proxy:3128".into()));
2828 assert!(base.resolved_proxy().unwrap().is_some());
2829 assert!(base
2830 .clone()
2831 .proxy_bypass_first_party(true)
2832 .resolved_proxy()
2833 .unwrap()
2834 .is_none());
2835 let public = ClientConfig::default()
2837 .proxy(ProxyMode::Explicit("http://proxy:3128".into()))
2838 .proxy_bypass_first_party(true);
2839 assert!(public.resolved_proxy().unwrap().is_some());
2840 assert!(ClientConfig::default().resolved_proxy().unwrap().is_none());
2841 }
2842
2843 #[test]
2844 fn invalid_explicit_proxy_fails_closed() {
2845 let config = ClientConfig::default().proxy(ProxyMode::Explicit("ftp://nope:1".into()));
2846 assert!(matches!(
2847 Client::try_new(config.clone()),
2848 Err(HttpError::InvalidRequest(_))
2849 ));
2850 let err = Client::new(config)
2851 .send(&Request::get("http://127.0.0.1:9/"))
2852 .unwrap_err();
2853 assert!(matches!(err, HttpError::InvalidRequest(_)), "{err:?}");
2854 }
2855
2856 fn split_settings() -> crate::system_proxy::SystemProxySettings {
2859 crate::system_proxy::SystemProxySettings {
2860 proxy: Some("http=plain.corp:8080;https=secure.corp:8443".into()),
2861 bypass: Some("<local>;*.corp.example;https://only-tls.example".into()),
2862 ..crate::system_proxy::SystemProxySettings::default()
2863 }
2864 }
2865
2866 #[test]
2867 fn per_scheme_proxies_are_chosen_per_request_url() {
2868 let routes = SystemRoutes::new(&split_settings()).unwrap();
2869 let at = |scheme: &str, host: &str| {
2870 routes
2871 .proxy_for(scheme, host)
2872 .map(|p| (p.host.clone(), p.port))
2873 };
2874 assert_eq!(
2875 at("http", "api.example.com"),
2876 Some(("plain.corp".into(), 8080))
2877 );
2878 assert_eq!(
2879 at("https", "api.example.com"),
2880 Some(("secure.corp".into(), 8443))
2881 );
2882 let mut endpoints = routes.endpoints();
2883 endpoints.sort();
2884 assert_eq!(
2885 endpoints,
2886 vec![
2887 ("plain.corp".to_string(), 8080),
2888 ("secure.corp".to_string(), 8443)
2889 ]
2890 );
2891 }
2892
2893 #[test]
2894 fn a_scheme_limited_bypass_entry_bypasses_that_scheme_only() {
2895 let routes = SystemRoutes::new(&split_settings()).unwrap();
2896 assert!(routes.proxy_for("https", "only-tls.example").is_none());
2897 assert_eq!(
2898 routes.proxy_for("http", "only-tls.example").map(|p| p.port),
2899 Some(8080)
2900 );
2901 }
2902
2903 #[test]
2904 fn local_and_wildcard_bypass_entries_go_direct_on_both_schemes() {
2905 let routes = SystemRoutes::new(&split_settings()).unwrap();
2906 for scheme in ["http", "https"] {
2907 assert!(
2908 routes.proxy_for(scheme, "intranet").is_none(),
2909 "{scheme} <local>"
2910 );
2911 assert!(
2912 routes.proxy_for(scheme, "a.corp.example").is_none(),
2913 "{scheme} *.corp"
2914 );
2915 assert!(
2916 routes.proxy_for(scheme, "intranet.other.com").is_some(),
2917 "{scheme}"
2918 );
2919 }
2920 }
2921
2922 #[test]
2923 fn a_scheme_without_an_endpoint_is_direct() {
2924 let settings = crate::system_proxy::SystemProxySettings {
2925 proxy: Some("https=secure.corp:8443".into()),
2926 ..crate::system_proxy::SystemProxySettings::default()
2927 };
2928 let routes = SystemRoutes::new(&settings).unwrap();
2929 assert!(routes.proxy_for("http", "x.example.com").is_none());
2930 assert!(routes.proxy_for("https", "x.example.com").is_some());
2931 }
2932
2933 fn recording_proxy() -> (u16, std::sync::Arc<std::sync::Mutex<Vec<String>>>) {
2935 use std::io::{BufRead, BufReader, Write};
2936 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2937 let port = listener.local_addr().unwrap().port();
2938 let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
2939 let log = seen.clone();
2940 std::thread::spawn(move || {
2941 for stream in listener.incoming() {
2942 let Ok(mut stream) = stream else { break };
2943 let mut first = String::new();
2944 let mut reader = BufReader::new(stream.try_clone().unwrap());
2945 let _ = reader.read_line(&mut first);
2946 log.lock().unwrap().push(first.trim().to_string());
2947 let _ = stream.write_all(
2948 b"HTTP/1.1 502 Bad Gateway\r\ncontent-length: 0\r\nconnection: close\r\n\r\n",
2949 );
2950 }
2951 });
2952 (port, seen)
2953 }
2954
2955 #[test]
2956 fn the_blocking_client_sends_each_request_to_its_own_scheme_proxy() {
2957 let (http_port, http_seen) = recording_proxy();
2958 let (https_port, https_seen) = recording_proxy();
2959 let settings = crate::system_proxy::SystemProxySettings {
2960 proxy: Some(format!(
2961 "http=127.0.0.1:{http_port};https=127.0.0.1:{https_port}"
2962 )),
2963 bypass: Some("<local>;https://tls-only.example".into()),
2964 ..crate::system_proxy::SystemProxySettings::default()
2965 };
2966 let config = ClientConfig {
2967 address_policy: AddressPolicy::default().with_resolver(|_| vec![]),
2968 retry: RetryPolicy::none(),
2969 request_timeout: Duration::from_secs(5),
2970 response_timeout: Some(Duration::from_secs(5)),
2971 connect_timeout: Some(Duration::from_secs(3)),
2972 ..ClientConfig::default()
2973 };
2974 let client = Client::build(
2975 config,
2976 Routing::PerRequest(Arc::new(SystemRoutes::new(&settings).unwrap())),
2977 ProxyModeUsed::System,
2978 );
2979 assert!(client.proxy_diagnostic().is_none());
2980 let go = |url: &str| {
2981 let _ = client.send(&Request::get(url));
2982 };
2983 go("http://app.example.com/");
2984 go("https://app.example.com/");
2985 go("http://tls-only.example/");
2986 go("https://tls-only.example/");
2988 go("http://intranet/");
2989 let http_seen = http_seen.lock().unwrap().clone();
2990 let https_seen = https_seen.lock().unwrap().clone();
2991 assert_eq!(
2992 http_seen,
2993 vec![
2994 "CONNECT app.example.com:80 HTTP/1.1".to_string(),
2995 "CONNECT tls-only.example:80 HTTP/1.1".to_string(),
2996 ]
2997 );
2998 assert_eq!(
2999 https_seen,
3000 vec!["CONNECT app.example.com:443 HTTP/1.1".to_string()]
3001 );
3002 }
3003
3004 #[test]
3005 fn component_encoding_and_repeated_headers() {
3006 assert_eq!(percent_encode_component("a b/c+~"), "a%20b%2Fc%2B~");
3007 assert_eq!(percent_decode_component("a%20b%2Fc+d%zz"), "a b/c+d%zz");
3008 let r = Response {
3009 status: 200,
3010 headers: vec![
3011 ("set-cookie".into(), "a=1".into()),
3012 ("set-cookie".into(), "b=2".into()),
3013 ],
3014 body: Vec::new(),
3015 attempts: 1,
3016 final_url: "https://x.test/".into(),
3017 redact: Vec::new(),
3018 };
3019 assert_eq!(
3020 r.header_all("Set-Cookie").collect::<Vec<_>>(),
3021 vec!["a=1", "b=2"]
3022 );
3023 assert_eq!(r.header("set-cookie"), Some("a=1"));
3024 assert_eq!(r.final_url(), "https://x.test/");
3025 }
3026}