From 8dd91862fb61ae09d9b622206bfb74efd8bd2e99 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=A9lix=20Saparelli?= <felix@passcod.name>
Date: Wed, 5 Feb 2025 00:30:31 +1300
Subject: [PATCH] feat(downloader): allow remote::Client to be customised
 (#2035)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This is for usage outside of binstall itself.

Sometimes it's useful to be able to specify more reqwest options, such as doing custom DNS resolution. This leaves the existing API identical but adds a couple methods that make it possible to construct a client with custom options.

Signed-off-by: Félix Saparelli <felix@passcod.name>
---
 crates/binstalk-downloader/src/remote.rs | 120 +++++++++++++----------
 1 file changed, 68 insertions(+), 52 deletions(-)

diff --git a/crates/binstalk-downloader/src/remote.rs b/crates/binstalk-downloader/src/remote.rs
index 1d1547fa..66fd365c 100644
--- a/crates/binstalk-downloader/src/remote.rs
+++ b/crates/binstalk-downloader/src/remote.rs
@@ -86,13 +86,17 @@ pub struct Client(Arc<Inner>);
 
 #[cfg_attr(not(feature = "__tls"), allow(unused_variables, unused_mut))]
 impl Client {
-    /// * `per_millis` - The duration (in millisecond) for which at most
-    ///   `num_request` can be sent, itcould be increased if rate-limit
-    ///   happens.
-    /// * `num_request` - maximum number of requests to be processed for
-    ///   each `per` duration.
+    /// Construct a new downloader client
     ///
-    /// The Client created would use at least tls 1.2
+    /// * `per_millis` - The duration (in millisecond) for which at most
+    ///   `num_request` can be sent. Increase it if rate-limit errors
+    ///   happen.
+    /// * `num_request` - maximum number of requests to be processed for
+    ///   each `per_millis` duration.
+    ///
+    /// The [`reqwest::Client`] constructed has secure defaults, such as allowing
+    /// only TLS v1.2 and above, and disallowing plaintext HTTP altogether. If you
+    /// need more control, use the `from_builder` variant.
     pub fn new(
         user_agent: impl AsRef<str>,
         min_tls: Option<TLSVersion>,
@@ -100,57 +104,69 @@ impl Client {
         num_request: NonZeroU64,
         certificates: impl IntoIterator<Item = Certificate>,
     ) -> Result<Self, Error> {
-        fn inner(
-            user_agent: &str,
-            min_tls: Option<TLSVersion>,
-            per_millis: NonZeroU16,
-            num_request: NonZeroU64,
-            certificates: &mut dyn Iterator<Item = Certificate>,
-        ) -> Result<Client, Error> {
-            let mut builder = reqwest::ClientBuilder::new()
-                .user_agent(user_agent)
-                .https_only(true)
-                .tcp_nodelay(false);
-
-            #[cfg(feature = "hickory-dns")]
-            {
-                builder = builder.dns_resolver(Arc::new(TrustDnsResolver::default()));
-            }
-
-            #[cfg(feature = "__tls")]
-            {
-                let tls_ver = min_tls
-                    .map(|tls| tls.max(DEFAULT_MIN_TLS))
-                    .unwrap_or(DEFAULT_MIN_TLS);
-
-                builder = builder.min_tls_version(tls_ver.into());
-
-                for certificate in certificates {
-                    builder = builder.add_root_certificate(certificate.0);
-                }
-            }
-
-            let client = builder.build()?;
-
-            Ok(Client(Arc::new(Inner {
-                client: client.clone(),
-                service: DelayRequest::new(
-                    num_request,
-                    Duration::from_millis(per_millis.get() as u64),
-                    client,
-                ),
-            })))
-        }
-
-        inner(
-            user_agent.as_ref(),
-            min_tls,
+        Self::from_builder(
+            Self::default_builder(user_agent.as_ref(), min_tls, &mut certificates.into_iter()),
             per_millis,
             num_request,
-            &mut certificates.into_iter(),
         )
     }
 
+    /// Constructs a default [`reqwest::ClientBuilder`].
+    ///
+    /// This may be used alongside [`Client::from_builder`] to start from reasonable
+    /// defaults, but still be able to customise the reqwest instance. Arguments are
+    /// as [`Client::new`], but without generic parameters.
+    pub fn default_builder(
+        user_agent: &str,
+        min_tls: Option<TLSVersion>,
+        certificates: &mut dyn Iterator<Item = Certificate>,
+    ) -> reqwest::ClientBuilder {
+        let mut builder = reqwest::ClientBuilder::new()
+            .user_agent(user_agent)
+            .https_only(true)
+            .tcp_nodelay(false);
+
+        #[cfg(feature = "hickory-dns")]
+        {
+            builder = builder.dns_resolver(Arc::new(TrustDnsResolver::default()));
+        }
+
+        #[cfg(feature = "__tls")]
+        {
+            let tls_ver = min_tls
+                .map(|tls| tls.max(DEFAULT_MIN_TLS))
+                .unwrap_or(DEFAULT_MIN_TLS);
+
+            builder = builder.min_tls_version(tls_ver.into());
+
+            for certificate in certificates {
+                builder = builder.add_root_certificate(certificate.0);
+            }
+        }
+
+        builder
+    }
+
+    /// Construct a custom client from a [`reqwest::ClientBuilder`].
+    ///
+    /// You may want to also use [`Client::default_builder`].
+    pub fn from_builder(
+        builder: reqwest::ClientBuilder,
+        per_millis: NonZeroU16,
+        num_request: NonZeroU64,
+    ) -> Result<Self, Error> {
+        let client = builder.build()?;
+
+        Ok(Client(Arc::new(Inner {
+            client: client.clone(),
+            service: DelayRequest::new(
+                num_request,
+                Duration::from_millis(per_millis.get() as u64),
+                client,
+            ),
+        })))
+    }
+
     /// Return inner reqwest client.
     pub fn get_inner(&self) -> &reqwest::Client {
         &self.0.client