use std::sync::Arc;
use bitreq::RequestExt;
use serde::de::DeserializeOwned;
use serde::Serialize;
use super::url_for_debug;
use crate::error::HttpError;
use crate::response::{RawResponse, Response};
#[derive(Clone)]
pub(crate) struct ProxyConfig {
url: url::Url,
matcher: Option<regex::Regex>,
}
impl std::fmt::Debug for ProxyConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProxyConfig")
.field("url", &url_for_debug(self.url.as_str()))
.field("matcher", &self.matcher)
.finish()
}
}
fn validate_proxy_url(url: &url::Url) -> Response<()> {
match url.scheme() {
"http" => Ok(()),
scheme => Err(HttpError::Proxy(format!(
"Unsupported proxy URL scheme for bitreq backend: {scheme}"
))),
}
}
pub(crate) fn apply_proxy_if_needed(
request: bitreq::Request,
url: &str,
proxy_config: &Option<ProxyConfig>,
) -> Response<bitreq::Request> {
if let Some(ref config) = proxy_config {
if let Some(ref matcher) = config.matcher {
if let Ok(parsed_url) = url::Url::parse(url) {
if let Some(host) = parsed_url.host_str() {
if matcher.is_match(host) {
let proxy = bitreq::Proxy::new_http(&config.url)
.map_err(|e| HttpError::Proxy(e.to_string()))?;
return Ok(request.with_proxy(proxy));
}
}
}
} else {
let proxy = bitreq::Proxy::new_http(&config.url)
.map_err(|e| HttpError::Proxy(e.to_string()))?;
return Ok(request.with_proxy(proxy));
}
}
Ok(request)
}
#[derive(Clone)]
pub struct HttpClient {
inner: Arc<bitreq::Client>,
proxy_config: Option<ProxyConfig>,
no_redirects: bool,
}
impl std::fmt::Debug for HttpClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HttpClient").finish()
}
}
impl HttpClient {
pub fn new() -> Self {
super::install_rustls_crypto_provider();
Self {
inner: Arc::new(bitreq::Client::new(10)),
proxy_config: None,
no_redirects: false,
}
}
pub(crate) fn from_parts(
client: Arc<bitreq::Client>,
proxy_config: Option<ProxyConfig>,
no_redirects: bool,
) -> Self {
super::install_rustls_crypto_provider();
Self {
inner: client,
proxy_config,
no_redirects,
}
}
pub fn builder() -> HttpClientBuilder {
HttpClientBuilder::default()
}
fn configure_request(&self, request: bitreq::Request, url: &str) -> Response<bitreq::Request> {
let request = apply_proxy_if_needed(request, url, &self.proxy_config)?;
Ok(if self.no_redirects {
request.with_max_redirects(0)
} else {
request
})
}
pub async fn fetch<R: DeserializeOwned>(&self, url: &str) -> Response<R> {
let request = bitreq::get(url);
let request = self.configure_request(request, url)?;
let response = request
.send_async_with_client(&self.inner)
.await
.map_err(HttpError::from)?;
RawResponse::new(response.status_code as u16, response.into_bytes()).json_or_status_error()
}
pub async fn post_json<B: Serialize, R: DeserializeOwned>(
&self,
url: &str,
body: &B,
) -> Response<R> {
let request = bitreq::post(url).with_json(body).map_err(HttpError::from)?;
let request = self.configure_request(request, url)?;
let response: bitreq::Response = request
.send_async_with_client(&self.inner)
.await
.map_err(HttpError::from)?;
RawResponse::new(response.status_code as u16, response.into_bytes()).json_or_status_error()
}
pub async fn post_form<F: Serialize, R: DeserializeOwned>(
&self,
url: &str,
form: &F,
) -> Response<R> {
let form_str = serde_urlencoded::to_string(form)
.map_err(|e| HttpError::Serialization(e.to_string()))?;
let request = bitreq::post(url)
.with_body(form_str.into_bytes())
.with_header("Content-Type", "application/x-www-form-urlencoded");
let request = self.configure_request(request, url)?;
let response: bitreq::Response = request
.send_async_with_client(&self.inner)
.await
.map_err(HttpError::from)?;
RawResponse::new(response.status_code as u16, response.into_bytes()).json_or_status_error()
}
pub async fn patch_json<B: Serialize, R: DeserializeOwned>(
&self,
url: &str,
body: &B,
) -> Response<R> {
let request = bitreq::patch(url)
.with_json(body)
.map_err(HttpError::from)?;
let request = self.configure_request(request, url)?;
let response: bitreq::Response = request
.send_async_with_client(&self.inner)
.await
.map_err(HttpError::from)?;
RawResponse::new(response.status_code as u16, response.into_bytes()).json_or_status_error()
}
pub async fn get_raw(&self, url: &str) -> Response<RawResponse> {
let request = bitreq::get(url);
let request = self.configure_request(request, url)?;
let response = request
.send_async_with_client(&self.inner)
.await
.map_err(HttpError::from)?;
Ok(RawResponse::new(
response.status_code as u16,
response.into_bytes(),
))
}
pub fn post(&self, url: &str) -> BitreqRequestBuilder {
BitreqRequestBuilder::new(
bitreq::post(url),
url,
self.inner.clone(),
self.proxy_config.clone(),
self.no_redirects,
)
}
pub fn get(&self, url: &str) -> BitreqRequestBuilder {
BitreqRequestBuilder::new(
bitreq::get(url),
url,
self.inner.clone(),
self.proxy_config.clone(),
self.no_redirects,
)
}
pub fn patch(&self, url: &str) -> BitreqRequestBuilder {
BitreqRequestBuilder::new(
bitreq::patch(url),
url,
self.inner.clone(),
self.proxy_config.clone(),
self.no_redirects,
)
}
pub fn put(&self, url: &str) -> BitreqRequestBuilder {
BitreqRequestBuilder::new(
bitreq::put(url),
url,
self.inner.clone(),
self.proxy_config.clone(),
self.no_redirects,
)
}
}
pub struct BitreqRequestBuilder {
inner: bitreq::Request,
error: Option<HttpError>,
url: String,
client: Arc<bitreq::Client>,
proxy_config: Option<ProxyConfig>,
no_redirects: bool,
}
impl std::fmt::Debug for BitreqRequestBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BitreqRequestBuilder")
.field("url", &url_for_debug(&self.url))
.field("error", &self.error)
.finish_non_exhaustive()
}
}
impl BitreqRequestBuilder {
pub(crate) fn new(
inner: bitreq::Request,
url: &str,
client: Arc<bitreq::Client>,
proxy_config: Option<ProxyConfig>,
no_redirects: bool,
) -> Self {
Self {
inner,
error: None,
url: url.to_string(),
client,
proxy_config,
no_redirects,
}
}
pub fn header(self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
Self {
inner: self.inner.with_header(key.as_ref(), value.as_ref()),
error: self.error,
url: self.url,
client: self.client,
proxy_config: self.proxy_config,
no_redirects: self.no_redirects,
}
}
pub fn json<T>(mut self, body: &T) -> Self
where
T: Serialize,
{
if self.error.is_some() {
return self;
}
match self.inner.clone().with_json(body) {
Ok(req) => self.inner = req,
Err(e) => self.error = Some(HttpError::from(e)),
}
self
}
pub fn form<T>(mut self, body: &T) -> Self
where
T: Serialize + ?Sized,
{
match serde_urlencoded::to_string(body) {
Ok(form_str) => {
self.inner = self
.inner
.with_body(form_str.into_bytes())
.with_header("Content-Type", "application/x-www-form-urlencoded");
}
Err(e) => self.error = Some(HttpError::Serialization(e.to_string())),
}
self
}
pub async fn send(self) -> Response<RawResponse> {
if let Some(err) = self.error {
return Err(err);
}
let request = apply_proxy_if_needed(self.inner, &self.url, &self.proxy_config)?;
let request = if self.no_redirects {
request.with_max_redirects(0)
} else {
request
};
let response = request
.send_async_with_client(&self.client)
.await
.map_err(HttpError::from)?;
Ok(RawResponse::new(
response.status_code as u16,
response.into_bytes(),
))
}
pub async fn send_json<R: DeserializeOwned>(self) -> Response<R> {
if let Some(err) = self.error {
return Err(err);
}
let request = apply_proxy_if_needed(self.inner, &self.url, &self.proxy_config)?;
let request = if self.no_redirects {
request.with_max_redirects(0)
} else {
request
};
let response = request
.send_async_with_client(&self.client)
.await
.map_err(HttpError::from)?;
RawResponse::new(response.status_code as u16, response.into_bytes()).json_or_status_error()
}
}
#[derive(Debug, Default)]
pub struct HttpClientBuilder {
proxy: Option<ProxyConfig>,
accept_invalid_certs: bool,
no_redirects: bool,
}
impl HttpClientBuilder {
pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
self.accept_invalid_certs = accept;
self
}
pub fn no_redirects(mut self) -> Self {
self.no_redirects = true;
self
}
pub fn proxy(mut self, url: url::Url) -> Self {
self.proxy = Some(ProxyConfig { url, matcher: None });
self
}
pub fn proxy_with_matcher(mut self, url: url::Url, pattern: &str) -> Response<Self> {
let matcher = regex::Regex::new(pattern)
.map_err(|e| HttpError::Proxy(format!("Invalid proxy pattern: {}", e)))?;
self.proxy = Some(ProxyConfig {
url,
matcher: Some(matcher),
});
Ok(self)
}
pub fn build(self) -> Response<HttpClient> {
if self.accept_invalid_certs {
return Err(HttpError::Build(
"danger_accept_invalid_certs is not supported".to_string(),
));
}
if let Some(proxy) = &self.proxy {
validate_proxy_url(&proxy.url)?;
}
Ok(HttpClient::from_parts(
Arc::new(bitreq::Client::new(10)),
self.proxy,
self.no_redirects,
))
}
}
#[cfg(test)]
mod tests {
use super::HttpClient;
#[test]
fn request_builder_debug_redacts_url_credentials() {
let secret = "bitreq-url-token-secret";
let url = format!("https://user:{secret}@mint.example.com/api?token={secret}");
let request = HttpClient::new().post(&url);
let debug = format!("{request:?}");
assert!(debug.contains("https://mint.example.com/api"));
assert!(!debug.contains(secret));
}
#[test]
fn client_builder_debug_redacts_proxy_credentials() {
let secret = "bitreq-proxy-password-secret";
let proxy = url::Url::parse(&format!("http://user:{secret}@proxy.example.com:8080"))
.expect("valid proxy URL");
let builder = HttpClient::builder().proxy(proxy);
let debug = format!("{builder:?}");
assert!(debug.contains("http://proxy.example.com:8080"));
assert!(!debug.contains(secret));
}
}