Skip to main content

cdk_http_client/backends/
bitreq_backend.rs

1//! bitreq-based backend implementation
2
3use 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/// HTTP client wrapper
63#[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    /// Create a new HTTP client with default settings
78    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    /// Create an HTTP client from pre-built parts
88    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    /// Create a new HTTP client builder
102    pub fn builder() -> HttpClientBuilder {
103        HttpClientBuilder::default()
104    }
105
106    /// Apply proxy and redirect settings to a request
107    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    /// GET request, returns JSON deserialized to R
117    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    /// POST with JSON body, returns JSON deserialized to R
128    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    /// POST with form data, returns JSON deserialized to R
144    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    /// PATCH with JSON body, returns JSON deserialized to R
164    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    /// GET request returning raw response body
182    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    /// POST request builder for complex cases
196    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    /// GET request builder for complex cases
207    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    /// PATCH request builder for complex cases
218    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    /// PUT request builder for complex cases
229    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
240/// bitreq-based RequestBuilder wrapper
241pub 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    /// Create a new BitreqRequestBuilder from a bitreq::Request
261    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    /// Add a header to the request.
278    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    /// Set the request body as JSON.
290    pub fn json<T>(mut self, body: &T) -> Self
291    where
292        T: Serialize,
293    {
294        // Preserve any error already set by an earlier builder step rather than
295        // clearing it on a successful serialization.
296        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    /// Set the request body as form data.
307    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    /// Send the request and return a raw response.
324    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    /// Send the request and deserialize the response as JSON.
345    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/// HTTP client builder for configuring proxy and TLS settings
365#[derive(Debug, Default)]
366pub struct HttpClientBuilder {
367    proxy: Option<ProxyConfig>,
368    accept_invalid_certs: bool,
369    no_redirects: bool,
370}
371
372impl HttpClientBuilder {
373    /// Accept invalid TLS certificates
374    pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
375        self.accept_invalid_certs = accept;
376        self
377    }
378
379    /// Disable automatic HTTP redirect following
380    pub fn no_redirects(mut self) -> Self {
381        self.no_redirects = true;
382        self
383    }
384
385    /// Set an HTTP proxy URL.
386    ///
387    /// The `bitreq` backend supports HTTP proxy URLs only. SOCKS proxy schemes
388    /// such as `socks5h` require building this crate with the `reqwest` feature.
389    pub fn proxy(mut self, url: url::Url) -> Self {
390        self.proxy = Some(ProxyConfig { url, matcher: None });
391        self
392    }
393
394    /// Set an HTTP proxy URL with a host pattern matcher.
395    ///
396    /// The `bitreq` backend supports HTTP proxy URLs only. SOCKS proxy schemes
397    /// such as `socks5h` require building this crate with the `reqwest` feature.
398    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    /// Build the HTTP client
409    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}