cdk_http_client/backends/
bitreq_backend.rs1use std::sync::Arc;
4
5use bitreq::RequestExt;
6use serde::de::DeserializeOwned;
7use serde::Serialize;
8
9use super::url_for_debug;
10use crate::error::HttpError;
11use crate::response::{RawResponse, Response};
12
13#[derive(Clone)]
14pub(crate) struct ProxyConfig {
15 url: url::Url,
16 matcher: Option<regex::Regex>,
17}
18
19impl std::fmt::Debug for ProxyConfig {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 f.debug_struct("ProxyConfig")
22 .field("url", &url_for_debug(self.url.as_str()))
23 .field("matcher", &self.matcher)
24 .finish()
25 }
26}
27
28fn validate_proxy_url(url: &url::Url) -> Response<()> {
29 match url.scheme() {
30 "http" => Ok(()),
31 scheme => Err(HttpError::Proxy(format!(
32 "Unsupported proxy URL scheme for bitreq backend: {scheme}"
33 ))),
34 }
35}
36
37pub(crate) fn apply_proxy_if_needed(
38 request: bitreq::Request,
39 url: &str,
40 proxy_config: &Option<ProxyConfig>,
41) -> Response<bitreq::Request> {
42 if let Some(ref config) = proxy_config {
43 if let Some(ref matcher) = config.matcher {
44 if let Ok(parsed_url) = url::Url::parse(url) {
45 if let Some(host) = parsed_url.host_str() {
46 if matcher.is_match(host) {
47 let proxy = bitreq::Proxy::new_http(&config.url)
48 .map_err(|e| HttpError::Proxy(e.to_string()))?;
49 return Ok(request.with_proxy(proxy));
50 }
51 }
52 }
53 } else {
54 let proxy = bitreq::Proxy::new_http(&config.url)
55 .map_err(|e| HttpError::Proxy(e.to_string()))?;
56 return Ok(request.with_proxy(proxy));
57 }
58 }
59 Ok(request)
60}
61
62#[derive(Clone)]
64pub struct HttpClient {
65 inner: Arc<bitreq::Client>,
66 proxy_config: Option<ProxyConfig>,
67 no_redirects: bool,
68}
69
70impl std::fmt::Debug for HttpClient {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 f.debug_struct("HttpClient").finish()
73 }
74}
75
76impl HttpClient {
77 pub fn new() -> Self {
79 super::install_rustls_crypto_provider();
80 Self {
81 inner: Arc::new(bitreq::Client::new(10)),
82 proxy_config: None,
83 no_redirects: false,
84 }
85 }
86
87 pub(crate) fn from_parts(
89 client: Arc<bitreq::Client>,
90 proxy_config: Option<ProxyConfig>,
91 no_redirects: bool,
92 ) -> Self {
93 super::install_rustls_crypto_provider();
94 Self {
95 inner: client,
96 proxy_config,
97 no_redirects,
98 }
99 }
100
101 pub fn builder() -> HttpClientBuilder {
103 HttpClientBuilder::default()
104 }
105
106 fn configure_request(&self, request: bitreq::Request, url: &str) -> Response<bitreq::Request> {
108 let request = apply_proxy_if_needed(request, url, &self.proxy_config)?;
109 Ok(if self.no_redirects {
110 request.with_max_redirects(0)
111 } else {
112 request
113 })
114 }
115
116 pub async fn fetch<R: DeserializeOwned>(&self, url: &str) -> Response<R> {
118 let request = bitreq::get(url);
119 let request = self.configure_request(request, url)?;
120 let response = request
121 .send_async_with_client(&self.inner)
122 .await
123 .map_err(HttpError::from)?;
124 RawResponse::new(response.status_code as u16, response.into_bytes()).json_or_status_error()
125 }
126
127 pub async fn post_json<B: Serialize, R: DeserializeOwned>(
129 &self,
130 url: &str,
131 body: &B,
132 ) -> Response<R> {
133 let request = bitreq::post(url).with_json(body).map_err(HttpError::from)?;
134 let request = self.configure_request(request, url)?;
135 let response: bitreq::Response = request
136 .send_async_with_client(&self.inner)
137 .await
138 .map_err(HttpError::from)?;
139
140 RawResponse::new(response.status_code as u16, response.into_bytes()).json_or_status_error()
141 }
142
143 pub async fn post_form<F: Serialize, R: DeserializeOwned>(
145 &self,
146 url: &str,
147 form: &F,
148 ) -> Response<R> {
149 let form_str = serde_urlencoded::to_string(form)
150 .map_err(|e| HttpError::Serialization(e.to_string()))?;
151 let request = bitreq::post(url)
152 .with_body(form_str.into_bytes())
153 .with_header("Content-Type", "application/x-www-form-urlencoded");
154 let request = self.configure_request(request, url)?;
155 let response: bitreq::Response = request
156 .send_async_with_client(&self.inner)
157 .await
158 .map_err(HttpError::from)?;
159
160 RawResponse::new(response.status_code as u16, response.into_bytes()).json_or_status_error()
161 }
162
163 pub async fn patch_json<B: Serialize, R: DeserializeOwned>(
165 &self,
166 url: &str,
167 body: &B,
168 ) -> Response<R> {
169 let request = bitreq::patch(url)
170 .with_json(body)
171 .map_err(HttpError::from)?;
172 let request = self.configure_request(request, url)?;
173 let response: bitreq::Response = request
174 .send_async_with_client(&self.inner)
175 .await
176 .map_err(HttpError::from)?;
177
178 RawResponse::new(response.status_code as u16, response.into_bytes()).json_or_status_error()
179 }
180
181 pub async fn get_raw(&self, url: &str) -> Response<RawResponse> {
183 let request = bitreq::get(url);
184 let request = self.configure_request(request, url)?;
185 let response = request
186 .send_async_with_client(&self.inner)
187 .await
188 .map_err(HttpError::from)?;
189 Ok(RawResponse::new(
190 response.status_code as u16,
191 response.into_bytes(),
192 ))
193 }
194
195 pub fn post(&self, url: &str) -> BitreqRequestBuilder {
197 BitreqRequestBuilder::new(
198 bitreq::post(url),
199 url,
200 self.inner.clone(),
201 self.proxy_config.clone(),
202 self.no_redirects,
203 )
204 }
205
206 pub fn get(&self, url: &str) -> BitreqRequestBuilder {
208 BitreqRequestBuilder::new(
209 bitreq::get(url),
210 url,
211 self.inner.clone(),
212 self.proxy_config.clone(),
213 self.no_redirects,
214 )
215 }
216
217 pub fn patch(&self, url: &str) -> BitreqRequestBuilder {
219 BitreqRequestBuilder::new(
220 bitreq::patch(url),
221 url,
222 self.inner.clone(),
223 self.proxy_config.clone(),
224 self.no_redirects,
225 )
226 }
227
228 pub fn put(&self, url: &str) -> BitreqRequestBuilder {
230 BitreqRequestBuilder::new(
231 bitreq::put(url),
232 url,
233 self.inner.clone(),
234 self.proxy_config.clone(),
235 self.no_redirects,
236 )
237 }
238}
239
240pub struct BitreqRequestBuilder {
242 inner: bitreq::Request,
243 error: Option<HttpError>,
244 url: String,
245 client: Arc<bitreq::Client>,
246 proxy_config: Option<ProxyConfig>,
247 no_redirects: bool,
248}
249
250impl std::fmt::Debug for BitreqRequestBuilder {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 f.debug_struct("BitreqRequestBuilder")
253 .field("url", &url_for_debug(&self.url))
254 .field("error", &self.error)
255 .finish_non_exhaustive()
256 }
257}
258
259impl BitreqRequestBuilder {
260 pub(crate) fn new(
262 inner: bitreq::Request,
263 url: &str,
264 client: Arc<bitreq::Client>,
265 proxy_config: Option<ProxyConfig>,
266 no_redirects: bool,
267 ) -> Self {
268 Self {
269 inner,
270 error: None,
271 url: url.to_string(),
272 client,
273 proxy_config,
274 no_redirects,
275 }
276 }
277 pub fn header(self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
279 Self {
280 inner: self.inner.with_header(key.as_ref(), value.as_ref()),
281 error: self.error,
282 url: self.url,
283 client: self.client,
284 proxy_config: self.proxy_config,
285 no_redirects: self.no_redirects,
286 }
287 }
288
289 pub fn json<T>(mut self, body: &T) -> Self
291 where
292 T: Serialize,
293 {
294 if self.error.is_some() {
297 return self;
298 }
299 match self.inner.clone().with_json(body) {
300 Ok(req) => self.inner = req,
301 Err(e) => self.error = Some(HttpError::from(e)),
302 }
303 self
304 }
305
306 pub fn form<T>(mut self, body: &T) -> Self
308 where
309 T: Serialize + ?Sized,
310 {
311 match serde_urlencoded::to_string(body) {
312 Ok(form_str) => {
313 self.inner = self
314 .inner
315 .with_body(form_str.into_bytes())
316 .with_header("Content-Type", "application/x-www-form-urlencoded");
317 }
318 Err(e) => self.error = Some(HttpError::Serialization(e.to_string())),
319 }
320 self
321 }
322
323 pub async fn send(self) -> Response<RawResponse> {
325 if let Some(err) = self.error {
326 return Err(err);
327 }
328 let request = apply_proxy_if_needed(self.inner, &self.url, &self.proxy_config)?;
329 let request = if self.no_redirects {
330 request.with_max_redirects(0)
331 } else {
332 request
333 };
334 let response = request
335 .send_async_with_client(&self.client)
336 .await
337 .map_err(HttpError::from)?;
338 Ok(RawResponse::new(
339 response.status_code as u16,
340 response.into_bytes(),
341 ))
342 }
343
344 pub async fn send_json<R: DeserializeOwned>(self) -> Response<R> {
346 if let Some(err) = self.error {
347 return Err(err);
348 }
349 let request = apply_proxy_if_needed(self.inner, &self.url, &self.proxy_config)?;
350 let request = if self.no_redirects {
351 request.with_max_redirects(0)
352 } else {
353 request
354 };
355 let response = request
356 .send_async_with_client(&self.client)
357 .await
358 .map_err(HttpError::from)?;
359
360 RawResponse::new(response.status_code as u16, response.into_bytes()).json_or_status_error()
361 }
362}
363
364#[derive(Debug, Default)]
366pub struct HttpClientBuilder {
367 proxy: Option<ProxyConfig>,
368 accept_invalid_certs: bool,
369 no_redirects: bool,
370}
371
372impl HttpClientBuilder {
373 pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
375 self.accept_invalid_certs = accept;
376 self
377 }
378
379 pub fn no_redirects(mut self) -> Self {
381 self.no_redirects = true;
382 self
383 }
384
385 pub fn proxy(mut self, url: url::Url) -> Self {
390 self.proxy = Some(ProxyConfig { url, matcher: None });
391 self
392 }
393
394 pub fn proxy_with_matcher(mut self, url: url::Url, pattern: &str) -> Response<Self> {
399 let matcher = regex::Regex::new(pattern)
400 .map_err(|e| HttpError::Proxy(format!("Invalid proxy pattern: {}", e)))?;
401 self.proxy = Some(ProxyConfig {
402 url,
403 matcher: Some(matcher),
404 });
405 Ok(self)
406 }
407
408 pub fn build(self) -> Response<HttpClient> {
410 if self.accept_invalid_certs {
411 return Err(HttpError::Build(
412 "danger_accept_invalid_certs is not supported".to_string(),
413 ));
414 }
415
416 if let Some(proxy) = &self.proxy {
417 validate_proxy_url(&proxy.url)?;
418 }
419
420 Ok(HttpClient::from_parts(
421 Arc::new(bitreq::Client::new(10)),
422 self.proxy,
423 self.no_redirects,
424 ))
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::HttpClient;
431
432 #[test]
433 fn request_builder_debug_redacts_url_credentials() {
434 let secret = "bitreq-url-token-secret";
435 let url = format!("https://user:{secret}@mint.example.com/api?token={secret}");
436 let request = HttpClient::new().post(&url);
437
438 let debug = format!("{request:?}");
439
440 assert!(debug.contains("https://mint.example.com/api"));
441 assert!(!debug.contains(secret));
442 }
443
444 #[test]
445 fn client_builder_debug_redacts_proxy_credentials() {
446 let secret = "bitreq-proxy-password-secret";
447 let proxy = url::Url::parse(&format!("http://user:{secret}@proxy.example.com:8080"))
448 .expect("valid proxy URL");
449 let builder = HttpClient::builder().proxy(proxy);
450
451 let debug = format!("{builder:?}");
452
453 assert!(debug.contains("http://proxy.example.com:8080"));
454 assert!(!debug.contains(secret));
455 }
456}