1use std::time::Duration;
9
10const DEFAULT_POOL_MAX_IDLE_PER_HOST: usize = 10;
12
13const DEFAULT_POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(90);
15
16#[derive(Debug, Clone, Default)]
22pub struct ProxyConfig {
23 pub url: Option<String>,
25 pub no_proxy: Option<String>,
27}
28
29impl ProxyConfig {
30 fn normalize(&mut self) {
31 if self.url.as_ref().is_some_and(|s| s.trim().is_empty()) {
32 self.url = None;
33 }
34 if self.no_proxy.as_ref().is_some_and(|s| s.trim().is_empty()) {
35 self.no_proxy = None;
36 }
37 }
38}
39
40#[derive(Debug)]
47pub struct HttpClient {
48 inner: reqwest::Client,
49 max_idle_per_host: usize,
50 idle_timeout: Duration,
51 proxy_config: ProxyConfig,
52}
53
54impl HttpClient {
55 pub fn new() -> Self {
62 HttpClientBuilder::new()
63 .build()
64 .expect("HttpClient construction should not fail with valid defaults")
65 }
66
67 pub fn client(&self) -> &reqwest::Client {
69 &self.inner
70 }
71
72 pub fn pool_config(&self) -> (usize, Duration) {
74 (self.max_idle_per_host, self.idle_timeout)
75 }
76
77 pub fn proxy_config(&self) -> &ProxyConfig {
79 &self.proxy_config
80 }
81}
82
83impl Default for HttpClient {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89pub struct HttpClientBuilder {
91 max_idle_per_host: usize,
92 idle_timeout: Duration,
93 proxy_config: ProxyConfig,
94}
95
96impl HttpClientBuilder {
97 pub fn new() -> Self {
99 Self {
100 max_idle_per_host: DEFAULT_POOL_MAX_IDLE_PER_HOST,
101 idle_timeout: DEFAULT_POOL_IDLE_TIMEOUT,
102 proxy_config: ProxyConfig::default(),
103 }
104 }
105
106 pub fn max_idle_per_host(mut self, n: usize) -> Self {
108 self.max_idle_per_host = n;
109 self
110 }
111
112 pub fn idle_timeout(mut self, d: Duration) -> Self {
114 self.idle_timeout = d;
115 self
116 }
117
118 pub fn proxy(mut self, config: ProxyConfig) -> Self {
123 self.proxy_config = config;
124 self.proxy_config.normalize();
125 self
126 }
127
128 pub fn build(self) -> Result<HttpClient, reqwest::Error> {
133 let mut builder = reqwest::Client::builder()
134 .pool_max_idle_per_host(self.max_idle_per_host)
135 .pool_idle_timeout(Some(self.idle_timeout));
136
137 if let Some(ref url) = self.proxy_config.url {
138 let mut proxy = reqwest::Proxy::all(url)?;
139 if let Some(ref np) = self.proxy_config.no_proxy {
140 proxy = proxy.no_proxy(reqwest::NoProxy::from_string(np));
141 }
142 builder = builder.proxy(proxy);
143 }
144
145 let inner = builder.build()?;
146 Ok(HttpClient {
147 inner,
148 max_idle_per_host: self.max_idle_per_host,
149 idle_timeout: self.idle_timeout,
150 proxy_config: self.proxy_config,
151 })
152 }
153}
154
155impl Default for HttpClientBuilder {
156 fn default() -> Self {
157 Self::new()
158 }
159}
160
161pub fn resolve_proxy(
167 http_proxy: Option<&str>,
168 https_proxy: Option<&str>,
169 no_proxy: Option<&str>,
170) -> ProxyConfig {
171 let url = https_proxy
172 .and_then(|s| {
173 if s.trim().is_empty() {
174 None
175 } else {
176 Some(s.to_string())
177 }
178 })
179 .or_else(|| {
180 http_proxy.and_then(|s| {
181 if s.trim().is_empty() {
182 None
183 } else {
184 Some(s.to_string())
185 }
186 })
187 });
188 let np = no_proxy.and_then(|s| {
189 if s.trim().is_empty() {
190 None
191 } else {
192 Some(s.to_string())
193 }
194 });
195 ProxyConfig { url, no_proxy: np }
196}
197
198fn env_var_case_insensitive(upper: &str, lower: &str) -> Option<String> {
200 std::env::var(upper)
201 .ok()
202 .or_else(|| std::env::var(lower).ok())
203}
204
205pub fn proxy_from_env() -> ProxyConfig {
211 let https_proxy = env_var_case_insensitive("HTTPS_PROXY", "https_proxy");
212 let http_proxy = env_var_case_insensitive("HTTP_PROXY", "http_proxy");
213 let no_proxy = env_var_case_insensitive("NO_PROXY", "no_proxy");
214 resolve_proxy(
215 http_proxy.as_deref(),
216 https_proxy.as_deref(),
217 no_proxy.as_deref(),
218 )
219}
220
221pub fn redact_proxy_credentials(url: &str) -> String {
226 if let Some(scheme_end) = url.find("://") {
227 let after_scheme = &url[scheme_end + 3..];
228 if let Some(at_pos) = after_scheme.find('@') {
229 let credentials = &after_scheme[..at_pos];
230 let host_part = &after_scheme[at_pos + 1..];
231 if credentials.contains(':') {
232 return format!("{}***:***@{}", &url[..scheme_end + 3], host_part);
233 }
234 return format!("{}***@{}", &url[..scheme_end + 3], host_part);
236 }
237 }
238 url.to_string()
239}