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(Clone, Default)]
87pub enum ProxyMode {
88 #[default]
90 None,
91 Environment,
93 Explicit(String),
95 System,
106}
107
108impl fmt::Debug for ProxyMode {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 match self {
111 ProxyMode::None => f.write_str("None"),
112 ProxyMode::Environment => f.write_str("Environment"),
113 ProxyMode::Explicit(url) => write!(f, "Explicit({})", redact_url(url)),
114 ProxyMode::System => f.write_str("System"),
115 }
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
127pub enum TrustRoots {
128 #[default]
132 Platform,
133 Bundled,
137}
138
139fn blocking_tls_config(trust: TrustRoots) -> ureq::tls::TlsConfig {
142 let roots = match trust {
143 TrustRoots::Platform => ureq::tls::RootCerts::PlatformVerifier,
144 TrustRoots::Bundled => {
145 let extra = crate::trust::test_roots();
146 if extra.is_empty() {
147 ureq::tls::RootCerts::WebPki
148 } else {
149 ureq::tls::RootCerts::Specific(std::sync::Arc::new(
150 extra
151 .iter()
152 .map(|d| ureq::tls::Certificate::from_der(d).to_owned())
153 .collect(),
154 ))
155 }
156 }
157 };
158 ureq::tls::TlsConfig::builder()
159 .provider(ureq::tls::TlsProvider::Rustls)
160 .root_certs(roots)
161 .build()
162}
163
164impl TrustRoots {
165 pub fn verifier_kind(self) -> &'static str {
167 match self {
168 TrustRoots::Platform => "platform",
169 TrustRoots::Bundled => "bundled",
170 }
171 }
172}
173
174pub(crate) struct ResolvedProxy {
176 pub(crate) host: String,
177 pub(crate) port: u16,
178 pub(crate) ureq: ureq::Proxy,
179 #[cfg_attr(not(feature = "async"), allow(dead_code))]
181 pub(crate) url: String,
182 #[allow(dead_code)]
184 pub(crate) no_proxy: Vec<String>,
185 pub(crate) from_env: bool,
187}
188
189#[derive(Clone, Debug, PartialEq, Eq)]
192pub struct ProxyDiagnostic {
193 pub endpoint: String,
195 pub from_environment: bool,
198 pub endpoint_exempt_from_class_check: bool,
202}
203
204impl ProxyDiagnostic {
205 pub(crate) fn of(p: &ResolvedProxy) -> Self {
206 Self {
207 endpoint: format!("{}://{}:{}", p.scheme(), p.host, p.port),
208 from_environment: p.from_env,
209 endpoint_exempt_from_class_check: true,
210 }
211 }
212}
213
214impl ResolvedProxy {
215 fn scheme(&self) -> &'static str {
216 if self.ureq.protocol() == ureq::ProxyProtocol::Https {
217 "https"
218 } else {
219 "http"
220 }
221 }
222}
223
224const PROXY_ENV: [&str; 6] = [
225 "ALL_PROXY",
226 "all_proxy",
227 "HTTPS_PROXY",
228 "https_proxy",
229 "HTTP_PROXY",
230 "http_proxy",
231];
232
233fn build_proxy(url: &str, no_proxy: &[String]) -> Result<ResolvedProxy, HttpError> {
235 let invalid = || HttpError::InvalidRequest("invalid proxy URL".into());
236 let base = ureq::Proxy::new(url).map_err(|_| invalid())?;
237 if !matches!(
238 base.protocol(),
239 ureq::ProxyProtocol::Http | ureq::ProxyProtocol::Https
240 ) {
241 return Err(invalid());
242 }
243 let mut builder = ureq::Proxy::builder(base.protocol())
244 .host(base.host())
245 .port(base.port())
246 .resolve_target(false);
247 if let Some(user) = base.username() {
248 builder = builder.username(user);
249 }
250 if let Some(pass) = base.password() {
251 builder = builder.password(pass);
252 }
253 for expr in no_proxy {
254 builder = builder.no_proxy(expr);
255 }
256 let ureq = builder.build().map_err(|_| invalid())?;
257 Ok(ResolvedProxy {
258 host: normalize_host(ureq.host()),
259 port: ureq.port(),
260 ureq,
261 url: url.to_string(),
262 no_proxy: no_proxy.to_vec(),
263 from_env: false,
264 })
265}
266
267pub(crate) fn proxy_from_lookup(
269 get: &dyn Fn(&str) -> Option<String>,
270) -> Result<Option<ResolvedProxy>, HttpError> {
271 let no_proxy: Vec<String> = ["NO_PROXY", "no_proxy"]
272 .iter()
273 .find_map(|name| get(name).filter(|v| !v.is_empty()))
274 .map(|v| {
275 v.split(',')
276 .map(|e| e.trim().to_string())
277 .filter(|e| !e.is_empty())
278 .collect()
279 })
280 .unwrap_or_default();
281 for name in PROXY_ENV {
282 if let Some(url) = get(name).filter(|v| !v.trim().is_empty()) {
283 if let Ok(mut proxy) = build_proxy(url.trim(), &no_proxy) {
284 proxy.from_env = true;
285 return Ok(Some(proxy));
286 }
287 }
288 }
289 Ok(None)
290}
291
292impl ClientConfig {
293 pub(crate) fn resolved_proxy(&self) -> Result<Option<ResolvedProxy>, HttpError> {
295 if self.proxy_bypass_first_party && self.address_policy.is_first_party() {
296 return Ok(None);
297 }
298 match &self.proxy {
299 ProxyMode::None => Ok(None),
300 ProxyMode::Explicit(url) => build_proxy(url, &[]).map(Some),
301 ProxyMode::Environment => proxy_from_lookup(&|name| std::env::var(name).ok()),
302 ProxyMode::System => {
303 match proxy_from_system(&crate::system_proxy::system_proxy_settings())? {
304 Some(proxy) => Ok(Some(proxy)),
305 None => proxy_from_lookup(&|name| std::env::var(name).ok()),
306 }
307 }
308 }
309 }
310}
311
312pub(crate) fn proxy_from_system(
321 settings: &crate::system_proxy::SystemProxySettings,
322) -> Result<Option<ResolvedProxy>, HttpError> {
323 use crate::system_proxy::SystemProxyChoice;
324 match crate::system_proxy::proxy_choice(settings) {
325 SystemProxyChoice::None => Ok(None),
326 SystemProxyChoice::Single { url, no_proxy } => {
327 Ok(build_proxy(&url, &no_proxy).ok().map(|mut proxy| {
328 proxy.from_env = true;
329 proxy
330 }))
331 }
332 SystemProxyChoice::Unsupported(element) => {
333 Err(HttpError::SystemProxyUnsupported { element })
334 }
335 }
336}
337
338#[derive(Clone)]
339pub struct ClientConfig {
340 pub user_agent: String,
341 pub connect_timeout: Option<Duration>,
343 pub request_timeout: Duration,
345 pub response_timeout: Option<Duration>,
348 pub recv_body_timeout: Option<Duration>,
351 pub stream_timeout: Option<Duration>,
353 pub max_redirects: u32,
355 pub max_redirects_will_error: bool,
359 pub max_response_bytes: usize,
360 pub retry: RetryPolicy,
361 pub on_retry: Option<RetryObserver>,
362 pub redact_values: Vec<String>,
365 pub address_policy: AddressPolicy,
369 pub proxy: ProxyMode,
372 pub proxy_bypass_first_party: bool,
375 pub trust: TrustRoots,
379 pub https_only: bool,
384}
385
386impl Default for ClientConfig {
387 fn default() -> Self {
388 Self {
389 user_agent: concat!("rightkit-http/", env!("CARGO_PKG_VERSION")).to_string(),
390 connect_timeout: Some(Duration::from_secs(10)),
391 request_timeout: Duration::from_secs(60),
392 response_timeout: Some(Duration::from_secs(60)),
393 recv_body_timeout: None,
394 stream_timeout: None,
395 max_redirects: 0,
396 max_redirects_will_error: true,
397 max_response_bytes: 32 * 1024 * 1024,
398 retry: RetryPolicy::default(),
399 on_retry: None,
400 redact_values: Vec::new(),
401 address_policy: AddressPolicy::default(),
402 proxy: ProxyMode::None,
403 proxy_bypass_first_party: false,
404 trust: TrustRoots::Platform,
405 https_only: false,
406 }
407 }
408}
409
410impl ClientConfig {
411 pub fn first_party(hosts: Vec<String>) -> Self {
415 Self::default().address_policy(AddressPolicy::first_party(hosts))
416 }
417
418 pub fn legacy_ureq_parity(user_agent: impl Into<String>) -> Self {
424 Self {
425 user_agent: user_agent.into(),
426 proxy: ProxyMode::Environment,
427 trust: TrustRoots::Bundled,
428 max_redirects: 10,
429 retry: RetryPolicy::none(),
430 ..Self::default()
431 }
432 }
433
434 pub fn address_policy(mut self, policy: AddressPolicy) -> Self {
436 self.address_policy = policy;
437 self
438 }
439
440 pub fn proxy(mut self, mode: ProxyMode) -> Self {
442 self.proxy = mode;
443 self
444 }
445
446 pub fn trust(mut self, trust: TrustRoots) -> Self {
448 self.trust = trust;
449 self
450 }
451
452 pub fn https_only(mut self, https_only: bool) -> Self {
454 self.https_only = https_only;
455 self
456 }
457
458 pub fn proxy_bypass_first_party(mut self, bypass: bool) -> Self {
461 self.proxy_bypass_first_party = bypass;
462 self
463 }
464
465 pub fn redact_values(mut self, values: Vec<String>) -> Self {
467 self.redact_values
468 .extend(values.into_iter().filter(|v| !v.is_empty()));
469 self
470 }
471}
472
473#[derive(Clone)]
474pub struct Request {
475 pub method: Method,
476 pub url: String,
477 pub headers: Vec<(String, String)>,
478 pub body: Vec<u8>,
479 pub timeout: Option<Duration>,
481 pub retry: Option<RetryPolicy>,
482 pub retry_unsafe_statuses: bool,
488 pub(crate) max_response_bytes: Option<usize>,
489 pub(crate) deadline: Option<Duration>,
490 pub(crate) cancel: Option<CancelToken>,
491 pub(crate) secret_values: Vec<String>,
492 pub(crate) headers_only: bool,
493}
494
495impl fmt::Debug for Request {
496 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
497 let headers: Vec<(String, String)> = self
498 .headers
499 .iter()
500 .map(|(k, v)| {
501 let shown = if redirect::is_secret_header_name(k) {
502 REDACTED.to_string()
503 } else {
504 redact_str(&redact_urls_in_text(v), &sorted_values(&self.secret_values))
505 };
506 (k.clone(), shown)
507 })
508 .collect();
509 f.debug_struct("Request")
510 .field("method", &self.method)
511 .field(
512 "url",
513 &redact_str(&redact_url(&self.url), &self.secret_values),
514 )
515 .field("headers", &headers)
516 .field("body_len", &self.body.len())
517 .field("timeout", &self.timeout)
518 .field("retry", &self.retry)
519 .field("retry_unsafe_statuses", &self.retry_unsafe_statuses)
520 .field("max_response_bytes", &self.max_response_bytes)
521 .field("deadline", &self.deadline)
522 .field("cancel", &self.cancel.is_some())
523 .field("headers_only", &self.headers_only)
524 .finish()
525 }
526}
527
528impl Request {
529 pub fn new(method: Method, url: impl Into<String>) -> Self {
530 Self {
531 method,
532 url: url.into(),
533 headers: Vec::new(),
534 body: Vec::new(),
535 timeout: None,
536 retry: None,
537 retry_unsafe_statuses: false,
538 max_response_bytes: None,
539 deadline: None,
540 cancel: None,
541 secret_values: Vec::new(),
542 headers_only: false,
543 }
544 }
545 pub fn get(url: impl Into<String>) -> Self {
546 Self::new(Method::Get, url)
547 }
548 pub fn post(url: impl Into<String>) -> Self {
549 Self::new(Method::Post, url)
550 }
551 pub fn put(url: impl Into<String>) -> Self {
552 Self::new(Method::Put, url)
553 }
554 pub fn delete(url: impl Into<String>) -> Self {
555 Self::new(Method::Delete, url)
556 }
557 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
559 self.headers.push((name.into(), value.into()));
560 self
561 }
562 pub fn set_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
565 let name = name.into();
566 let value = value.into();
567 let position = self
568 .headers
569 .iter()
570 .position(|(k, _)| k.eq_ignore_ascii_case(&name));
571 self.headers.retain(|(k, _)| !k.eq_ignore_ascii_case(&name));
572 match position {
573 Some(i) => self.headers.insert(i, (name, value)),
574 None => self.headers.push((name, value)),
575 }
576 self
577 }
578 pub fn secret_header(self, name: impl Into<String>, value: impl Into<String>) -> Self {
581 let value = value.into();
582 let mut req = self;
583 if !value.is_empty() {
584 req.secret_values.push(value.clone());
585 }
586 req.set_header(name, value)
587 }
588 pub fn bearer(mut self, token: &str) -> Self {
590 if !token.is_empty() {
591 self.secret_values.push(token.to_string());
592 }
593 self.header("Authorization", format!("Bearer {token}"))
594 }
595 pub fn redact_value(mut self, value: impl Into<String>) -> Self {
599 let value = value.into();
600 if !value.is_empty() {
601 self.secret_values.push(value);
602 }
603 self
604 }
605 pub fn secret_form(mut self, pairs: &[(&str, &str)], secret_names: &[&str]) -> Self {
609 for (name, value) in pairs {
610 if secret_names.iter().any(|s| s.eq_ignore_ascii_case(name)) && !value.is_empty() {
611 self.secret_values.push((*value).to_string());
612 }
613 }
614 self.form(pairs)
615 }
616 pub fn scrub(&self, text: &str) -> String {
620 redact_str(text, &sorted_values(&self.secret_values))
621 }
622 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
623 self.body = body.into();
624 self
625 }
626 pub fn json<T: Serialize>(mut self, value: &T) -> Result<Self, HttpError> {
627 self.body = serde_json::to_vec(value).map_err(json_error)?;
628 self.headers
629 .push(("Content-Type".into(), "application/json".into()));
630 Ok(self)
631 }
632 pub fn form(mut self, pairs: &[(&str, &str)]) -> Self {
634 self.body = form_encode(pairs).into_bytes();
635 self.headers.push((
636 "Content-Type".into(),
637 "application/x-www-form-urlencoded".into(),
638 ));
639 self
640 }
641 pub fn timeout(mut self, d: Duration) -> Self {
644 self.timeout = Some(d);
645 self
646 }
647 pub fn deadline(mut self, total: Duration) -> Self {
651 self.deadline = Some(total);
652 self
653 }
654 pub fn retry(mut self, policy: RetryPolicy) -> Self {
655 self.retry = Some(policy);
656 self
657 }
658 pub fn retry_unsafe_statuses(mut self, allow: bool) -> Self {
660 self.retry_unsafe_statuses = allow;
661 self
662 }
663 pub fn max_response_bytes(mut self, limit: usize) -> Self {
665 self.max_response_bytes = Some(limit);
666 self
667 }
668 pub fn headers_only(mut self) -> Self {
673 self.headers_only = true;
674 self
675 }
676 pub fn cancel(mut self, token: CancelToken) -> Self {
679 self.cancel = Some(token);
680 self
681 }
682}
683
684pub fn percent_encode(input: &str) -> String {
686 let mut out = String::with_capacity(input.len());
687 for b in input.bytes() {
688 match b {
689 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
690 out.push(b as char)
691 }
692 b' ' => out.push('+'),
693 _ => out.push_str(&format!("%{b:02X}")),
694 }
695 }
696 out
697}
698
699pub fn percent_decode(input: &str) -> String {
700 let bytes = input.as_bytes();
701 let mut out = Vec::with_capacity(bytes.len());
702 let mut i = 0;
703 while i < bytes.len() {
704 match bytes[i] {
705 b'+' => out.push(b' '),
706 b'%' if i + 2 < bytes.len() => {
707 let hi = (bytes[i + 1] as char).to_digit(16);
708 let lo = (bytes[i + 2] as char).to_digit(16);
709 match (hi, lo) {
710 (Some(h), Some(l)) => {
711 out.push((h * 16 + l) as u8);
712 i += 2;
713 }
714 _ => out.push(b'%'),
715 }
716 }
717 b => out.push(b),
718 }
719 i += 1;
720 }
721 String::from_utf8_lossy(&out).into_owned()
722}
723
724pub fn form_encode(pairs: &[(&str, &str)]) -> String {
725 pairs
726 .iter()
727 .map(|(k, v)| format!("{}={}", percent_encode(k), percent_encode(v)))
728 .collect::<Vec<_>>()
729 .join("&")
730}
731
732#[derive(Clone)]
733pub struct Response {
734 pub status: u16,
735 pub headers: Vec<(String, String)>,
737 pub body: Vec<u8>,
738 pub attempts: u32,
739 pub(crate) redact: Vec<String>,
742}
743
744impl Response {
745 pub fn header(&self, name: &str) -> Option<&str> {
746 header_of(&self.headers, name)
747 }
748 pub fn text(&self) -> String {
749 String::from_utf8_lossy(&self.body).into_owned()
750 }
751 pub fn json<T: DeserializeOwned>(&self) -> Result<T, HttpError> {
754 serde_json::from_slice(&self.body).map_err(json_error)
755 }
756}
757
758pub(crate) fn json_error(e: serde_json::Error) -> HttpError {
760 HttpError::Json(format!(
761 "{:?} error at line {} column {}",
762 e.classify(),
763 e.line(),
764 e.column()
765 ))
766}
767
768impl fmt::Debug for Response {
769 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
770 f.debug_struct("Response")
771 .field("status", &self.status)
772 .field("headers", &redact_headers_with(&self.headers, &self.redact))
773 .field("body_len", &self.body.len())
774 .field("attempts", &self.attempts)
775 .finish()
776 }
777}
778
779#[derive(Clone)]
782pub struct ResponseHead {
783 pub status: u16,
784 pub headers: Vec<(String, String)>,
786 pub final_url: String,
789 pub hops: u32,
791 pub attempts: u32,
793 pub(crate) redact: Vec<String>,
795}
796
797impl ResponseHead {
798 pub fn header(&self, name: &str) -> Option<&str> {
799 header_of(&self.headers, name)
800 }
801}
802
803impl fmt::Debug for ResponseHead {
804 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
805 f.debug_struct("ResponseHead")
806 .field("status", &self.status)
807 .field("headers", &redact_headers_with(&self.headers, &self.redact))
808 .field(
809 "final_url",
810 &redact_str(&redact_url(&self.final_url), &self.redact),
811 )
812 .field("hops", &self.hops)
813 .field("attempts", &self.attempts)
814 .finish()
815 }
816}
817
818pub struct StreamResponse {
819 pub status: u16,
820 pub headers: Vec<(String, String)>,
821 pub attempts: u32,
822 reader: Box<dyn Read + Send>,
823}
824
825impl StreamResponse {
826 pub fn header(&self, name: &str) -> Option<&str> {
827 header_of(&self.headers, name)
828 }
829 pub fn into_reader(self) -> Box<dyn Read + Send> {
830 self.reader
831 }
832 pub fn sse(self) -> SseReader<Box<dyn Read + Send>> {
834 SseReader::new(self.reader)
835 }
836}
837
838pub(crate) fn header_of<'a>(headers: &'a [(String, String)], name: &str) -> Option<&'a str> {
839 headers
840 .iter()
841 .find(|(k, _)| k.eq_ignore_ascii_case(name))
842 .map(|(_, v)| v.as_str())
843}
844
845#[derive(Clone)]
846pub struct Client {
847 agent: ureq::Agent,
848 config: ClientConfig,
849 proxy: Option<ureq::Proxy>,
851 proxy_diagnostic: Option<ProxyDiagnostic>,
852 config_error: Option<String>,
854 system_proxy_unsupported: Option<String>,
858}
859
860pub(crate) struct Raw {
861 pub(crate) status: u16,
862 pub(crate) headers: Vec<(String, String)>,
863 pub(crate) body: ureq::Body,
864 pub(crate) attempts: u32,
865 pub(crate) final_url: String,
867 pub(crate) hops: u32,
869}
870
871impl Client {
872 pub fn new(config: ClientConfig) -> Self {
875 match Self::try_new(config.clone()) {
876 Ok(client) => client,
877 Err(e) => {
878 let mut direct = config;
879 direct.proxy = ProxyMode::None;
880 let mut client = Self::build(direct, None);
881 if let HttpError::SystemProxyUnsupported { element } = &e {
882 client.system_proxy_unsupported = Some(element.clone());
883 }
884 client.config_error = Some(e.to_string());
885 client
886 }
887 }
888 }
889
890 pub fn try_new(config: ClientConfig) -> Result<Self, HttpError> {
892 let proxy = config.resolved_proxy()?;
893 Ok(Self::build(config, proxy))
894 }
895
896 fn build(config: ClientConfig, proxy: Option<ResolvedProxy>) -> Self {
897 let builder = ureq::Agent::config_builder()
900 .http_status_as_error(false)
901 .max_redirects(0)
902 .user_agent(config.user_agent.clone())
903 .timeout_connect(config.connect_timeout)
904 .timeout_resolve(config.connect_timeout)
905 .tls_config(blocking_tls_config(config.trust));
906 let diagnostic = proxy.as_ref().map(ProxyDiagnostic::of);
907 let (builder, endpoint, proxy) = match proxy {
908 Some(p) => (
909 builder.proxy(Some(p.ureq.clone())),
910 Some((p.host, p.port)),
911 Some(p.ureq),
912 ),
913 None => (builder.proxy(None), None, None),
914 };
915 let agent = ureq::Agent::with_parts(
919 builder.build(),
920 ureq::unversioned::transport::DefaultConnector::default(),
921 VettingResolver {
922 policy: Arc::new(config.address_policy.clone()),
923 proxy: endpoint,
924 lookup_limit: config.connect_timeout.unwrap_or(Duration::from_secs(10)),
925 },
926 );
927 Self {
928 agent,
929 config,
930 proxy,
931 proxy_diagnostic: diagnostic,
932 config_error: None,
933 system_proxy_unsupported: None,
934 }
935 }
936
937 pub fn verifier_kind(&self) -> &'static str {
940 self.config.trust.verifier_kind()
941 }
942
943 pub fn proxy_diagnostic(&self) -> Option<&ProxyDiagnostic> {
945 self.proxy_diagnostic.as_ref()
946 }
947
948 pub fn first_party(hosts: Vec<String>) -> Self {
951 Self::new(ClientConfig::first_party(hosts))
952 }
953
954 pub fn config(&self) -> &ClientConfig {
955 &self.config
956 }
957
958 pub fn send(&self, req: &Request) -> Result<Response, HttpError> {
960 let resp = self.send_any(req)?;
961 if (200..300).contains(&resp.status) {
962 Ok(resp)
963 } else {
964 let values = self.redaction_values(req);
965 Err(scrub_error(
966 status_error(resp.status, &resp.headers, resp.body, resp.attempts),
967 &values,
968 ))
969 }
970 }
971
972 pub fn send_any(&self, req: &Request) -> Result<Response, HttpError> {
974 let timeout = req.timeout.unwrap_or(self.config.request_timeout);
975 let cancel = request_cancel(req);
976 let mut raw = self.execute(req, Some(timeout), cancel)?;
977 let values = self.redaction_values(req);
978 if req.headers_only {
979 return Ok(Response {
981 status: raw.status,
982 headers: raw.headers,
983 body: Vec::new(),
984 attempts: raw.attempts,
985 redact: values,
986 });
987 }
988 let limit = req
989 .max_response_bytes
990 .unwrap_or(self.config.max_response_bytes);
991 let mut body_reader = raw.body.as_reader();
992 let mut reader = CheckedReader {
993 inner: &mut body_reader,
994 cancel,
995 };
996 let body = read_limited(&mut reader, limit).map_err(|e| scrub_error(e, &values))?;
997 Ok(Response {
998 status: raw.status,
999 headers: raw.headers,
1000 body,
1001 attempts: raw.attempts,
1002 redact: values,
1003 })
1004 }
1005
1006 pub fn send_headers(&self, req: &Request) -> Result<ResponseHead, HttpError> {
1011 let timeout = req.timeout.unwrap_or(self.config.request_timeout);
1012 let raw = self.execute(req, Some(timeout), request_cancel(req))?;
1013 let Raw {
1014 status,
1015 headers,
1016 body,
1017 attempts,
1018 final_url,
1019 hops,
1020 ..
1021 } = raw;
1022 drop(body);
1023 Ok(ResponseHead {
1024 status,
1025 headers,
1026 final_url,
1027 hops,
1028 attempts,
1029 redact: self.redaction_values(req),
1030 })
1031 }
1032
1033 pub fn stream_any(&self, req: &Request) -> Result<StreamResponse, HttpError> {
1038 let total = req.timeout.or(self.config.stream_timeout);
1039 let raw = self.execute(req, total, request_cancel(req))?;
1040 let values = self.redaction_values(req);
1041 Ok(Self::wrap_stream(raw, req, values))
1042 }
1043
1044 fn wrap_stream(raw: Raw, req: &Request, values: Vec<String>) -> StreamResponse {
1045 let guarded = GuardReader {
1046 inner: Box::new(raw.body.into_reader()),
1047 cancel: req.cancel.clone(),
1048 values,
1049 };
1050 StreamResponse {
1051 status: raw.status,
1052 headers: raw.headers,
1053 attempts: raw.attempts,
1054 reader: Box::new(guarded),
1055 }
1056 }
1057
1058 pub fn stream(&self, req: &Request) -> Result<StreamResponse, HttpError> {
1064 let cancel = request_cancel(req);
1065 let total = req.timeout.or(self.config.stream_timeout);
1066 let mut raw = self.execute(req, total, cancel)?;
1067 let values = self.redaction_values(req);
1068 if !(200..300).contains(&raw.status) {
1069 let mut body_reader = raw.body.as_reader();
1070 let mut reader = CheckedReader {
1071 inner: &mut body_reader,
1072 cancel,
1073 };
1074 let body = read_limited(&mut reader, MAX_ERROR_BODY_BYTES).unwrap_or_default();
1075 if is_set(cancel) {
1076 return Err(HttpError::Cancelled);
1077 }
1078 return Err(scrub_error(
1079 status_error(raw.status, &raw.headers, body, raw.attempts),
1080 &values,
1081 ));
1082 }
1083 Ok(Self::wrap_stream(raw, req, values))
1084 }
1085
1086 pub fn get_json<T: DeserializeOwned>(&self, url: &str) -> Result<T, HttpError> {
1087 let req = Request::get(url).header("Accept", "application/json");
1088 let values = self.redaction_values(&req);
1089 self.send(&req)?.json().map_err(|e| scrub_error(e, &values))
1090 }
1091
1092 pub(crate) fn redaction_values(&self, req: &Request) -> Vec<String> {
1094 redaction_values_for(&self.config, req)
1095 }
1096
1097 fn execute(
1100 &self,
1101 req: &Request,
1102 total: Option<Duration>,
1103 cancel: Option<&AtomicBool>,
1104 ) -> Result<Raw, HttpError> {
1105 let values = self.redaction_values(req);
1106 self.execute_inner(req, total, cancel, &values)
1107 .map_err(|e| scrub_error(e, &values))
1108 }
1109
1110 fn execute_inner(
1111 &self,
1112 req: &Request,
1113 total: Option<Duration>,
1114 cancel: Option<&AtomicBool>,
1115 values: &[String],
1116 ) -> Result<Raw, HttpError> {
1117 let policy = req
1118 .retry
1119 .clone()
1120 .unwrap_or_else(|| self.config.retry.clone());
1121 let max = policy.max_attempts.max(1);
1122 let deadline = req.deadline.map(|d| Instant::now() + d);
1123 let mut attempt = 0u32;
1124 loop {
1125 if is_set(cancel) {
1126 return Err(HttpError::Cancelled);
1127 }
1128 let attempt_total = match deadline {
1129 None => total,
1130 Some(dl) => {
1131 let remaining = dl.saturating_duration_since(Instant::now());
1132 if remaining.is_zero() {
1133 return Err(HttpError::timeout(
1134 format!("request deadline exceeded after {attempt} attempt(s)"),
1135 Phase::Headers,
1136 ));
1137 }
1138 Some(total.map_or(remaining, |t| t.min(remaining)))
1139 }
1140 };
1141 attempt += 1;
1142 let (reason, delay) = match self.once(req, attempt_total, values) {
1143 Ok(raw) => {
1144 let retryable = policy.is_retryable_status(raw.status)
1145 && (req.method.is_idempotent() || req.retry_unsafe_statuses);
1146 let retry_after =
1147 header_of(&raw.headers, "retry-after").and_then(parse_retry_after);
1148 if !retryable || attempt >= max {
1149 return Ok(Raw {
1150 attempts: attempt,
1151 ..raw
1152 });
1153 }
1154 let delay = policy.delay(attempt, retry_after);
1155 if deadline_passes(deadline, delay) {
1156 return Ok(Raw {
1157 attempts: attempt,
1158 ..raw
1159 });
1160 }
1161 let mut body = raw.body;
1163 let mut reader = body.as_reader();
1164 let mut checked = CheckedReader {
1165 inner: &mut reader,
1166 cancel,
1167 };
1168 let _ = read_limited(&mut checked, MAX_ERROR_BODY_BYTES);
1169 if is_set(cancel) {
1170 return Err(HttpError::Cancelled);
1171 }
1172 (format!("HTTP {}", raw.status), delay)
1173 }
1174 Err(err) => {
1175 let retryable = match &err {
1176 HttpError::Transport { connect_phase, .. } => {
1177 *connect_phase || req.method.is_idempotent()
1178 }
1179 HttpError::Timeout { .. } => req.method.is_idempotent(),
1180 _ => false,
1181 };
1182 if !retryable || attempt >= max {
1183 return Err(err);
1184 }
1185 let delay = policy.delay(attempt, None);
1186 if deadline_passes(deadline, delay) {
1187 return Err(err);
1188 }
1189 (err.to_string(), delay)
1190 }
1191 };
1192 if let Some(obs) = &self.config.on_retry {
1193 obs(&RetryEvent {
1194 url: redact_str(&redact_url(&req.url), values),
1195 retry: attempt,
1196 reason: redact_str(&redact_urls_in_text(&reason), values),
1197 delay,
1198 });
1199 }
1200 let delay = match deadline {
1203 Some(dl) => delay.min(dl.saturating_duration_since(Instant::now())),
1204 None => delay,
1205 };
1206 if !sleep_cancellable(delay, cancel) {
1207 return Err(HttpError::Cancelled);
1208 }
1209 }
1210 }
1211
1212 fn once(
1215 &self,
1216 req: &Request,
1217 total: Option<Duration>,
1218 secrets: &[String],
1219 ) -> Result<Raw, HttpError> {
1220 let started = Instant::now();
1221 let mut method = req.method;
1222 let mut url = req.url.clone();
1223 let mut headers = req.headers.clone();
1224 let mut body = req.body.clone();
1225 let mut hops = 0u32;
1226 loop {
1227 let remaining = match total {
1228 None => None,
1229 Some(t) => {
1230 let left = t.saturating_sub(started.elapsed());
1231 if left.is_zero() {
1232 let phase = if hops > 0 {
1233 Phase::Redirect
1234 } else {
1235 Phase::Headers
1236 };
1237 return Err(HttpError::timeout("request", phase));
1238 }
1239 Some(left)
1240 }
1241 };
1242 let raw = match self.once_hop(method, &url, &headers, &body, remaining, hops > 0) {
1243 Ok(raw) => raw,
1244 Err(e) if hops > 0 => return Err(e.into_redirect_phase()),
1245 Err(e) => return Err(e),
1246 };
1247 if self.config.max_redirects == 0 || !redirect::is_redirect_status(raw.status) {
1248 return Ok(Raw {
1249 final_url: url,
1250 hops,
1251 ..raw
1252 });
1253 }
1254 let Some(location) = header_of(&raw.headers, "location") else {
1255 return Ok(Raw {
1256 final_url: url,
1257 hops,
1258 ..raw
1259 });
1260 };
1261 let Some(next_method) = redirect::redirect_method(raw.status, method) else {
1262 return Ok(Raw {
1263 final_url: url,
1264 hops,
1265 ..raw
1266 });
1267 };
1268 if hops >= self.config.max_redirects {
1269 if self.config.max_redirects_will_error {
1270 return Err(HttpError::transport("too many redirects", Phase::Redirect));
1271 }
1272 return Ok(Raw {
1273 final_url: url,
1274 hops,
1275 ..raw
1276 });
1277 }
1278 hops += 1;
1279 let next_url = redirect::resolve_location(&url, location).ok_or_else(|| {
1280 HttpError::InvalidRequest("redirect with an unusable Location".into())
1281 })?;
1282 if let (Some(from), Some(to), Some(host)) = (
1283 redirect::scheme_of(&url),
1284 redirect::scheme_of(&next_url),
1285 redirect::host_of(&next_url),
1286 ) {
1287 redirect::require_https(self.config.https_only, &to, &host, true)?;
1290 self.config
1291 .address_policy
1292 .check_redirect_scheme(&from, &to, &host)
1293 .map_err(|d| HttpError::from(d).into_redirect_phase())?;
1294 }
1295 if redirect::origin(&url) != redirect::origin(&next_url) {
1296 redirect::strip_cross_origin(&mut headers, secrets);
1297 }
1298 if next_method != method {
1299 body.clear();
1300 redirect::strip_body_headers(&mut headers);
1301 }
1302 method = next_method;
1303 url = next_url;
1304 }
1306 }
1307
1308 fn once_hop(
1309 &self,
1310 method: Method,
1311 url: &str,
1312 headers: &[(String, String)],
1313 body: &[u8],
1314 total: Option<Duration>,
1315 redirect_hop: bool,
1316 ) -> Result<Raw, HttpError> {
1317 let mut builder = ureq::http::Request::builder()
1318 .method(method.as_str())
1319 .uri(url);
1320 for (k, v) in headers {
1321 builder = builder.header(k.as_str(), v.as_str());
1322 }
1323 let bodiless =
1327 body.is_empty() && !matches!(method, Method::Post | Method::Put | Method::Patch);
1328 if bodiless {
1329 let request = builder
1330 .body(())
1331 .map_err(|_| HttpError::InvalidRequest("request could not be built".into()))?;
1332 self.run_hop(request, total, redirect_hop)
1333 } else {
1334 let request = builder
1335 .body(body.to_vec())
1336 .map_err(|_| HttpError::InvalidRequest("request could not be built".into()))?;
1337 self.run_hop(request, total, redirect_hop)
1338 }
1339 }
1340
1341 fn run_hop<S: ureq::AsSendBody>(
1343 &self,
1344 request: ureq::http::Request<S>,
1345 total: Option<Duration>,
1346 redirect_hop: bool,
1347 ) -> Result<Raw, HttpError> {
1348 if let Some(element) = &self.system_proxy_unsupported {
1349 return Err(HttpError::SystemProxyUnsupported {
1350 element: element.clone(),
1351 });
1352 }
1353 if let Some(reason) = &self.config_error {
1354 return Err(HttpError::InvalidRequest(reason.clone()));
1355 }
1356 redirect::require_https(
1359 self.config.https_only,
1360 request.uri().scheme_str().unwrap_or(""),
1361 request.uri().host().unwrap_or(""),
1362 redirect_hop,
1363 )?;
1364 let via_proxy = self.vet_proxied_destination(request.uri())?;
1365 let _scope = ViaProxy::enter(via_proxy);
1366 let configured = self
1367 .agent
1368 .configure_request(request)
1369 .timeout_global(total)
1370 .timeout_recv_response(self.config.response_timeout)
1371 .timeout_recv_body(self.config.recv_body_timeout)
1372 .build();
1373 let response = self.agent.run(configured).map_err(map_ureq_error)?;
1374 let status = response.status().as_u16();
1375 let headers = response
1376 .headers()
1377 .iter()
1378 .map(|(k, v)| {
1379 (
1380 k.as_str().to_ascii_lowercase(),
1381 String::from_utf8_lossy(v.as_bytes()).into_owned(),
1382 )
1383 })
1384 .collect();
1385 Ok(Raw {
1386 status,
1387 headers,
1388 body: response.into_body(),
1389 attempts: 1,
1390 final_url: String::new(),
1391 hops: 0,
1392 })
1393 }
1394}
1395
1396impl Client {
1397 fn vet_proxied_destination(&self, uri: &ureq::http::Uri) -> Result<bool, HttpError> {
1402 let Some(proxy) = &self.proxy else {
1403 return Ok(false);
1404 };
1405 if proxy.is_no_proxy(uri) {
1406 return Ok(false);
1407 }
1408 let policy = &self.config.address_policy;
1409 let scheme = uri.scheme_str().unwrap_or("").to_ascii_lowercase();
1410 let raw_host = uri.host().unwrap_or("");
1411 policy.admit(&scheme, raw_host)?;
1412 policy.vet_via_proxy(raw_host)?;
1413 Ok(true)
1414 }
1415}
1416
1417fn sorted_values(values: &[String]) -> Vec<String> {
1418 let mut v: Vec<String> = values.iter().filter(|v| !v.is_empty()).cloned().collect();
1419 v.sort_by_key(|s| std::cmp::Reverse(s.len()));
1420 v
1421}
1422
1423pub(crate) fn redact_headers_for_debug(headers: &[(String, String)]) -> Vec<(String, String)> {
1427 headers
1428 .iter()
1429 .map(|(k, v)| {
1430 let shown = if redirect::is_secret_header_name(k) {
1431 REDACTED.to_string()
1432 } else if matches!(
1433 k.to_ascii_lowercase().as_str(),
1434 "location" | "content-location" | "refresh" | "link" | "referer"
1435 ) {
1436 redact_urls_in_text(&redact_url(v))
1437 } else {
1438 redact_urls_in_text(v)
1439 };
1440 (k.clone(), shown)
1441 })
1442 .collect()
1443}
1444
1445pub(crate) fn redact_headers_with(
1447 headers: &[(String, String)],
1448 values: &[String],
1449) -> Vec<(String, String)> {
1450 redact_headers_for_debug(headers)
1451 .into_iter()
1452 .map(|(k, v)| (k, redact_str(&v, values)))
1453 .collect()
1454}
1455
1456pub(crate) fn redaction_values_for(config: &ClientConfig, req: &Request) -> Vec<String> {
1459 let mut values: Vec<String> = config
1460 .redact_values
1461 .iter()
1462 .chain(req.secret_values.iter())
1463 .filter(|v| !v.is_empty())
1464 .cloned()
1465 .chain(url_secret_parts(&req.url))
1466 .collect();
1467 values.sort_by_key(|v| std::cmp::Reverse(v.len()));
1468 values.dedup();
1469 values
1470}
1471
1472fn request_cancel(req: &Request) -> Option<&AtomicBool> {
1473 req.cancel.as_ref().map(CancelToken::as_atomic)
1474}
1475
1476fn deadline_passes(deadline: Option<Instant>, delay: Duration) -> bool {
1477 deadline.is_some_and(|dl| Instant::now() + delay >= dl)
1478}
1479
1480struct GuardReader {
1483 inner: Box<dyn Read + Send>,
1484 cancel: Option<CancelToken>,
1485 values: Vec<String>,
1486}
1487
1488impl Read for GuardReader {
1489 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1490 if self.cancel.as_ref().is_some_and(|c| c.is_cancelled()) {
1491 return Err(crate::cancel::cancelled_io());
1492 }
1493 self.inner.read(buf).map_err(|e| {
1494 if is_cancelled_io(&e) {
1495 return e;
1496 }
1497 let text = e.to_string();
1498 let scrubbed = redact_str(&redact_urls_in_text(&text), &self.values);
1499 if scrubbed == text {
1500 e
1501 } else {
1502 io::Error::new(e.kind(), scrubbed)
1503 }
1504 })
1505 }
1506}
1507
1508pub(crate) fn status_error(
1509 status: u16,
1510 headers: &[(String, String)],
1511 body: Vec<u8>,
1512 attempts: u32,
1513) -> HttpError {
1514 let mut body = body;
1515 body.truncate(MAX_ERROR_BODY_BYTES);
1516 HttpError::Status {
1517 status,
1518 body: String::from_utf8_lossy(&body).into_owned(),
1519 retry_after: header_of(headers, "retry-after").and_then(parse_retry_after),
1520 attempts,
1521 }
1522}
1523
1524pub(crate) fn redact_str(text: &str, values: &[String]) -> String {
1526 let mut out = text.to_string();
1527 for value in values {
1528 if !value.is_empty() && out.contains(value.as_str()) {
1529 out = out.replace(value.as_str(), REDACTED);
1530 }
1531 }
1532 out
1533}
1534
1535pub(crate) fn scrub_error(err: HttpError, values: &[String]) -> HttpError {
1537 let r = |s: String| redact_str(&redact_urls_in_text(&s), values);
1540 match err {
1541 HttpError::InvalidRequest(m) => HttpError::InvalidRequest(r(m)),
1542 HttpError::Transport {
1543 message,
1544 connect_phase,
1545 phase,
1546 } => HttpError::Transport {
1547 message: r(message),
1548 connect_phase,
1549 phase,
1550 },
1551 HttpError::Timeout { message, phase } => HttpError::Timeout {
1552 message: r(message),
1553 phase,
1554 },
1555 HttpError::Status {
1556 status,
1557 body,
1558 retry_after,
1559 attempts,
1560 } => HttpError::Status {
1561 status,
1562 body: redact_str(&body, values),
1563 retry_after,
1564 attempts,
1565 },
1566 HttpError::BodyTooLarge { limit } => HttpError::BodyTooLarge { limit },
1567 HttpError::Json(m) => HttpError::Json(r(m)),
1568 HttpError::Stream(m) => HttpError::Stream(r(m)),
1569 HttpError::Cancelled => HttpError::Cancelled,
1570 HttpError::SystemProxyUnsupported { element } => HttpError::SystemProxyUnsupported {
1571 element: element.clone(),
1572 },
1573 HttpError::HttpsRequired {
1574 host,
1575 scheme,
1576 redirect,
1577 } => HttpError::HttpsRequired {
1578 host: r(host),
1579 scheme,
1580 redirect,
1581 },
1582 HttpError::AddressDenied {
1583 host,
1584 addr,
1585 class,
1586 reason,
1587 } => HttpError::AddressDenied {
1588 host: r(host),
1589 addr,
1590 class,
1591 reason: r(reason),
1592 },
1593 }
1594}
1595
1596pub(crate) fn read_limited(reader: &mut dyn Read, limit: usize) -> Result<Vec<u8>, HttpError> {
1597 let mut out = Vec::new();
1598 reader
1599 .take(limit as u64 + 1)
1600 .read_to_end(&mut out)
1601 .map_err(|e| {
1602 if is_cancelled_io(&e) {
1603 return HttpError::Cancelled;
1604 }
1605 match e.kind() {
1606 std::io::ErrorKind::TimedOut => HttpError::timeout(e.to_string(), Phase::Body),
1607 _ => map_ureq_error_in(e.into(), Phase::Body),
1610 }
1611 })?;
1612 if out.len() > limit {
1613 return Err(HttpError::BodyTooLarge { limit });
1614 }
1615 Ok(out)
1616}
1617
1618pub(crate) fn map_ureq_error(err: ureq::Error) -> HttpError {
1620 map_ureq_error_in(err, Phase::Headers)
1621}
1622
1623fn timeout_phase(t: &ureq::Timeout, default: Phase) -> Phase {
1625 use ureq::Timeout as T;
1626 match t {
1627 T::Resolve | T::Connect => Phase::Connect,
1628 T::SendRequest | T::SendBody | T::RecvResponse => Phase::Headers,
1629 T::RecvBody => Phase::Body,
1630 _ => default,
1631 }
1632}
1633
1634pub(crate) fn map_ureq_error_in(err: ureq::Error, default: Phase) -> HttpError {
1637 use ureq::Error as E;
1638 if let E::Io(io) = &err {
1640 if let Some(inner) = io.get_ref() {
1641 if let Some(denial) = denial_in_chain(inner) {
1642 return denial.into();
1643 }
1644 }
1645 }
1646 match err {
1647 E::Timeout(t) => HttpError::timeout(format!("{t:?}"), timeout_phase(&t, default)),
1648 E::TooManyRedirects => HttpError::transport("too many redirects", Phase::Redirect),
1649 E::HostNotFound => HttpError::transport("host not found", Phase::Connect),
1650 E::ConnectionFailed => HttpError::transport("connection failed", Phase::Connect),
1651 E::Io(io)
1652 if matches!(
1653 io.kind(),
1654 std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::NotFound
1655 ) =>
1656 {
1657 HttpError::transport(io.to_string(), Phase::Connect)
1658 }
1659 E::Io(io) => HttpError::transport(io.to_string(), default),
1660 E::BadUri(m) => HttpError::InvalidRequest(m),
1661 E::Http(e) => HttpError::InvalidRequest(e.to_string()),
1662 other => HttpError::transport(other.to_string(), default),
1663 }
1664}
1665
1666#[cfg(test)]
1667mod tests {
1668 use super::*;
1669
1670 #[test]
1671 fn legacy_ureq_parity_sets_only_the_ureq_fields() {
1672 let cfg = ClientConfig::legacy_ureq_parity("heardright/1.0");
1673 let base = ClientConfig::default();
1674 assert_eq!(cfg.user_agent, "heardright/1.0");
1675 assert!(matches!(cfg.proxy, ProxyMode::Environment));
1676 assert_eq!(cfg.trust, TrustRoots::Bundled);
1677 assert_eq!(cfg.max_redirects, 10);
1678 assert_eq!(cfg.retry, RetryPolicy::none());
1679 assert_eq!(
1681 format!("{:?}", cfg.address_policy),
1682 format!("{:?}", base.address_policy)
1683 );
1684 assert_eq!(cfg.connect_timeout, base.connect_timeout);
1686 assert_eq!(cfg.request_timeout, base.request_timeout);
1687 assert_eq!(cfg.response_timeout, base.response_timeout);
1688 assert_eq!(cfg.recv_body_timeout, base.recv_body_timeout);
1689 assert_eq!(cfg.stream_timeout, base.stream_timeout);
1690 assert_eq!(cfg.max_redirects_will_error, base.max_redirects_will_error);
1691 assert_eq!(cfg.max_response_bytes, base.max_response_bytes);
1692 assert!(cfg.on_retry.is_none());
1693 assert!(cfg.redact_values.is_empty());
1694 assert_eq!(cfg.proxy_bypass_first_party, base.proxy_bypass_first_party);
1695 assert_eq!(cfg.https_only, base.https_only);
1696 }
1697
1698 #[test]
1699 fn put_and_delete_set_their_methods() {
1700 assert_eq!(Request::put("http://x/").method, Method::Put);
1701 assert_eq!(Request::delete("http://x/").method, Method::Delete);
1702 assert!(Request::put("http://x/").body.is_empty());
1703 }
1704
1705 #[test]
1706 fn set_header_replaces_case_insensitively_in_place() {
1707 let req = Request::post("http://x/")
1708 .header("X-A", "1")
1709 .header("Accept", "text/plain")
1710 .header("x-a", "2")
1711 .set_header("X-A", "3");
1712 assert_eq!(
1713 req.headers,
1714 vec![
1715 ("X-A".to_string(), "3".to_string()),
1716 ("Accept".to_string(), "text/plain".to_string()),
1717 ]
1718 );
1719 let req = req.set_header("content-type", "text/csv");
1720 assert_eq!(
1721 req.headers.last(),
1722 Some(&("content-type".to_string(), "text/csv".to_string()))
1723 );
1724 }
1725
1726 #[test]
1727 fn redaction_replaces_values_and_longest_first_scrub() {
1728 let values = vec!["secret-token-123".to_string(), "secret".to_string()];
1729 assert_eq!(
1730 redact_str("bad secret-token-123 and secret", &values),
1731 "bad [redacted] and [redacted]"
1732 );
1733 }
1734
1735 #[test]
1736 fn secret_header_hidden_in_debug_but_sent() {
1737 let req = Request::get("http://x/").secret_header("X-Key", "hunter2-value");
1738 assert!(req.headers.iter().any(|(_, v)| v == "hunter2-value"));
1739 let debug = format!("{req:?}");
1740 assert!(!debug.contains("hunter2-value"), "{debug}");
1741 assert!(debug.contains(REDACTED));
1742 }
1743
1744 #[test]
1745 fn scrub_error_covers_text_variants() {
1746 let values = vec!["tok-9".to_string()];
1747 match scrub_error(
1748 HttpError::Status {
1749 status: 401,
1750 body: "invalid tok-9".into(),
1751 retry_after: None,
1752 attempts: 1,
1753 },
1754 &values,
1755 ) {
1756 HttpError::Status { body, .. } => assert_eq!(body, "invalid [redacted]"),
1757 other => panic!("unexpected {other:?}"),
1758 }
1759 }
1760
1761 fn env_of(pairs: &'static [(&'static str, &'static str)]) -> impl Fn(&str) -> Option<String> {
1762 move |name| {
1763 pairs
1764 .iter()
1765 .find(|(k, _)| *k == name)
1766 .map(|(_, v)| v.to_string())
1767 }
1768 }
1769
1770 fn uri(s: &str) -> ureq::http::Uri {
1771 s.parse().unwrap()
1772 }
1773
1774 #[test]
1775 fn environment_proxy_uses_the_first_set_variable_and_skips_empty_ones() {
1776 let env = env_of(&[
1777 ("ALL_PROXY", ""),
1778 ("HTTPS_PROXY", "http://secure.proxy:3128"),
1779 ("HTTP_PROXY", "http://plain.proxy:8080"),
1780 ("NO_PROXY", "a.test, .b.test ,"),
1781 ]);
1782 let p = proxy_from_lookup(&env).unwrap().unwrap();
1783 assert_eq!(p.host, "secure.proxy");
1784 assert_eq!(p.port, 3128);
1785 assert_eq!(
1786 p.no_proxy,
1787 vec!["a.test".to_string(), ".b.test".to_string()]
1788 );
1789 assert!(p.ureq.is_no_proxy(&uri("http://a.test/")));
1790 assert!(p.ureq.is_no_proxy(&uri("http://x.b.test/")));
1791 assert!(!p.ureq.is_no_proxy(&uri("http://c.test/")));
1792 }
1793
1794 #[test]
1795 fn environment_proxy_order_and_case() {
1796 let env = env_of(&[
1797 ("http_proxy", "http://lower.proxy:1"),
1798 ("ALL_PROXY", "http://all.proxy:2"),
1799 ]);
1800 assert_eq!(proxy_from_lookup(&env).unwrap().unwrap().host, "all.proxy");
1801 let env = env_of(&[("http_proxy", "http://lower.proxy:1")]);
1802 assert_eq!(
1803 proxy_from_lookup(&env).unwrap().unwrap().host,
1804 "lower.proxy"
1805 );
1806 let env = env_of(&[("no_proxy", "z.test"), ("https_proxy", "http://p.proxy:9")]);
1807 assert_eq!(
1808 proxy_from_lookup(&env).unwrap().unwrap().no_proxy,
1809 vec!["z.test".to_string()]
1810 );
1811 }
1812
1813 #[test]
1814 fn environment_without_a_usable_proxy_is_none() {
1815 assert!(proxy_from_lookup(&env_of(&[])).unwrap().is_none());
1816 let env = env_of(&[
1818 ("ALL_PROXY", "ftp://nope.proxy:21"),
1819 ("HTTP_PROXY", "http://ok.proxy:80"),
1820 ]);
1821 assert_eq!(proxy_from_lookup(&env).unwrap().unwrap().host, "ok.proxy");
1822 assert!(
1823 proxy_from_lookup(&env_of(&[("ALL_PROXY", "ftp://nope.proxy:21")]))
1824 .unwrap()
1825 .is_none()
1826 );
1827 }
1828
1829 #[test]
1830 fn explicit_proxy_lets_the_proxy_resolve_and_keeps_credentials() {
1831 let p = build_proxy("http://user:pass@Proxy.Corp:3128", &[]).unwrap();
1832 assert_eq!(p.host, "proxy.corp");
1833 assert_eq!(p.port, 3128);
1834 assert!(!p.ureq.resolve_target());
1835 assert_eq!(p.ureq.username(), Some("user"));
1836 assert_eq!(p.ureq.password(), Some("pass"));
1837 assert!(build_proxy("https://10.0.0.5:3128", &[]).is_ok());
1838 assert!(build_proxy("socks5://127.0.0.1:1080", &[]).is_err());
1839 assert!(build_proxy("ftp://x:1", &[]).is_err());
1840 assert!(build_proxy("not a url", &[]).is_err());
1841 }
1842
1843 #[test]
1844 fn proxy_mode_debug_never_shows_credentials() {
1845 let shown = format!(
1846 "{:?}",
1847 ProxyMode::Explicit("http://user:pw-9@proxy:3128/x?k=v".into())
1848 );
1849 assert!(
1850 !shown.contains("pw-9") && !shown.contains("user:") && !shown.contains("k=v"),
1851 "{shown}"
1852 );
1853 assert_eq!(format!("{:?}", ProxyMode::None), "None");
1854 assert_eq!(format!("{:?}", ProxyMode::Environment), "Environment");
1855 assert_eq!(format!("{:?}", ProxyMode::System), "System");
1856 }
1857
1858 #[test]
1859 fn system_settings_resolve_like_an_environment_proxy() {
1860 use crate::system_proxy::SystemProxySettings;
1861 let proxy = proxy_from_system(&SystemProxySettings {
1862 proxy: Some("http=web.corp:8080;https=web.corp:8080".into()),
1863 bypass: Some("*.corp.example;intranet".into()),
1864 ..SystemProxySettings::default()
1865 })
1866 .unwrap()
1867 .expect("a static system proxy resolves");
1868 assert_eq!((proxy.host.as_str(), proxy.port), ("web.corp", 8080));
1869 assert_eq!(
1870 proxy.no_proxy,
1871 vec![".corp.example".to_string(), "intranet".to_string()]
1872 );
1873 assert!(ProxyDiagnostic::of(&proxy).from_environment);
1874 assert!(proxy_from_system(&SystemProxySettings::default())
1876 .unwrap()
1877 .is_none());
1878 }
1879
1880 #[test]
1881 fn system_settings_this_client_cannot_follow_exactly_fail_closed() {
1882 use crate::system_proxy::SystemProxySettings;
1883 let static_proxy = |proxy: &str, bypass: Option<&str>| SystemProxySettings {
1884 proxy: Some(proxy.into()),
1885 bypass: bypass.map(str::to_string),
1886 ..SystemProxySettings::default()
1887 };
1888 let rows = [
1892 (
1893 static_proxy("http=web.corp:8080;https=secure.corp:8443", None),
1894 "HTTP and HTTPS are given different proxy routes",
1895 ),
1896 (
1897 static_proxy("https=secure.corp:8443", None),
1898 "HTTP and HTTPS are given different proxy routes",
1899 ),
1900 (
1901 static_proxy("http=web.corp:8080", None),
1902 "HTTP and HTTPS are given different proxy routes",
1903 ),
1904 (
1905 static_proxy("web.corp:8080", Some("http://private.example")),
1906 "the bypass entry `http://private.example` cannot be followed exactly",
1907 ),
1908 (
1909 static_proxy("web.corp:8080", Some("*.corp.example;https://only.example")),
1910 "the bypass entry `https://only.example` cannot be followed exactly",
1911 ),
1912 (
1913 static_proxy("web.corp:8080", Some("<local>")),
1914 "the bypass entry `<local>` cannot be followed exactly",
1915 ),
1916 (
1917 static_proxy("web.corp:8080", Some("10.*")),
1918 "the bypass entry `10.*` cannot be followed exactly",
1919 ),
1920 (
1921 SystemProxySettings {
1922 auto_config: true,
1923 ..SystemProxySettings::default()
1924 },
1925 "a proxy auto-config script is set",
1926 ),
1927 (
1928 SystemProxySettings {
1929 auto_detect: true,
1930 ..SystemProxySettings::default()
1931 },
1932 "\"Automatically detect settings\" (proxy auto-detect) is on",
1933 ),
1934 ];
1935 for (settings, expected) in rows {
1936 let error = proxy_from_system(&settings)
1937 .err()
1938 .unwrap_or_else(|| panic!("{expected}: must be refused"));
1939 assert!(
1940 matches!(&error, HttpError::SystemProxyUnsupported { element } if element == expected),
1941 "{expected}: {error}"
1942 );
1943 assert_eq!(
1944 error.to_string(),
1945 format!(
1946 "the Windows proxy settings cannot be followed exactly by this version \
1947 ({expected}); nothing was sent"
1948 )
1949 );
1950 assert!(matches!(error.class(), crate::error::ErrorClass::Invalid));
1951 }
1952 }
1953
1954 #[test]
1955 fn first_party_bypass_drops_the_proxy_only_when_asked() {
1956 let base = ClientConfig::first_party(vec!["api.example.com".into()])
1957 .proxy(ProxyMode::Explicit("http://proxy:3128".into()));
1958 assert!(base.resolved_proxy().unwrap().is_some());
1959 assert!(base
1960 .clone()
1961 .proxy_bypass_first_party(true)
1962 .resolved_proxy()
1963 .unwrap()
1964 .is_none());
1965 let public = ClientConfig::default()
1967 .proxy(ProxyMode::Explicit("http://proxy:3128".into()))
1968 .proxy_bypass_first_party(true);
1969 assert!(public.resolved_proxy().unwrap().is_some());
1970 assert!(ClientConfig::default().resolved_proxy().unwrap().is_none());
1971 }
1972
1973 #[test]
1974 fn invalid_explicit_proxy_fails_closed() {
1975 let config = ClientConfig::default().proxy(ProxyMode::Explicit("ftp://nope:1".into()));
1976 assert!(matches!(
1977 Client::try_new(config.clone()),
1978 Err(HttpError::InvalidRequest(_))
1979 ));
1980 let err = Client::new(config)
1981 .send(&Request::get("http://127.0.0.1:9/"))
1982 .unwrap_err();
1983 assert!(matches!(err, HttpError::InvalidRequest(_)), "{err:?}");
1984 }
1985}