hyper_custom_cert/
lib.rs

1//! hyper-custom-cert
2//!
3//! A reusable HTTP client library that provides:
4//! - A small, ergonomic wrapper surface for building HTTP clients
5//! - A dev-only option to accept self-signed/invalid certificates (feature-gated)
6//! - A production-grade path to trust a custom Root CA by providing PEM bytes
7//! - Clear security boundaries and feature flags
8//!
9//! Note: Networking internals are intentionally abstracted for now; this crate
10//! focuses on a robust and secure configuration API surfaced via a builder.
11//!
12//! WebAssembly support and limitations
13//! -----------------------------------
14//! For wasm32 targets, this crate currently exposes API stubs that return
15//! `ClientError::WasmNotImplemented` when attempting to perform operations that
16//! would require configuring a TLS client with a custom Root CA. This is by design:
17//!
18//! Browsers do not allow web applications to programmatically install or trust
19//! custom Certificate Authorities. Trust decisions are enforced by the browser and
20//! the underlying OS. As a result, while native builds can securely add a custom
21//! Root CA (e.g., via `with_root_ca_pem` behind the `rustls` feature), the same is
22//! not possible in the browser environment. Any runtime method that would require
23//! such behavior will return `WasmNotImplemented` on wasm targets.
24//!
25//! If you need to target WebAssembly, build with `--no-default-features` to avoid
26//! pulling in native TLS dependencies, and expect stubbed behavior until a future
27//! browser capability or design change enables safe support.
28
29use std::collections::HashMap;
30use std::error::Error as StdError;
31use std::fmt;
32#[cfg(feature = "rustls")]
33use std::fs;
34#[cfg(feature = "rustls")]
35use std::path::Path;
36use std::time::Duration;
37
38use bytes::Bytes;
39use http_body_util::BodyExt;
40use hyper::{body::Incoming, Method, Request, Response, StatusCode, Uri};
41use hyper_util::client::legacy::Client;
42use hyper_util::rt::TokioExecutor;
43
44/// Options for controlling HTTP requests.
45///
46/// This struct provides a flexible interface for configuring individual
47/// HTTP requests without modifying the client's default settings.
48///
49/// # Examples
50///
51/// Adding custom headers to a specific request:
52///
53/// ```
54/// use hyper_custom_cert::{HttpClient, RequestOptions};
55/// use std::collections::HashMap;
56///
57/// // Create request-specific headers
58/// let mut headers = HashMap::new();
59/// headers.insert("x-request-id".to_string(), "123456".to_string());
60///
61/// // Create request options with these headers
62/// let options = RequestOptions::new()
63///     .with_headers(headers);
64///
65/// // Make request with custom options
66/// # async {
67/// let client = HttpClient::new();
68/// let _response = client.request_with_options("https://example.com", Some(options)).await;
69/// # };
70/// ```
71#[derive(Default, Clone)]
72pub struct RequestOptions {
73    /// Headers to add to this specific request
74    pub headers: Option<HashMap<String, String>>,
75    /// Override the client's default timeout for this request
76    pub timeout: Option<Duration>,
77}
78
79impl RequestOptions {
80    /// Create a new empty RequestOptions with default values.
81    pub fn new() -> Self {
82        RequestOptions::default()
83    }
84
85    /// Add custom headers to this request.
86    pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
87        self.headers = Some(headers);
88        self
89    }
90
91    /// Override the client's default timeout for this request.
92    pub fn with_timeout(mut self, timeout: Duration) -> Self {
93        self.timeout = Some(timeout);
94        self
95    }
96}
97
98/// HTTP response with raw body data exposed as bytes.
99#[derive(Debug, Clone)]
100pub struct HttpResponse {
101    /// HTTP status code
102    pub status: StatusCode,
103    /// Response headers
104    pub headers: HashMap<String, String>,
105    /// Raw response body as bytes - exposed without any permutations
106    pub body: Bytes,
107}
108
109/// Error type for this crate's runtime operations.
110#[derive(Debug)]
111pub enum ClientError {
112    /// Returned on wasm32 targets where runtime operations requiring custom CA
113    /// trust are not available due to browser security constraints.
114    WasmNotImplemented,
115    /// HTTP request failed
116    HttpError(hyper::Error),
117    /// HTTP request building failed
118    HttpBuildError(hyper::http::Error),
119    /// HTTP client request failed
120    HttpClientError(hyper_util::client::legacy::Error),
121    /// Invalid URI
122    InvalidUri(hyper::http::uri::InvalidUri),
123    /// TLS/Connection error
124    #[cfg(any(feature = "native-tls", feature = "rustls"))]
125    TlsError(String),
126    /// IO error (e.g., reading CA files)
127    IoError(std::io::Error),
128}
129
130impl fmt::Display for ClientError {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        match self {
133            ClientError::WasmNotImplemented => write!(
134                f,
135                "Not implemented on WebAssembly (browser restricts programmatic CA trust)"
136            ),
137            ClientError::HttpError(err) => write!(f, "HTTP error: {}", err),
138            ClientError::HttpBuildError(err) => write!(f, "HTTP build error: {}", err),
139            ClientError::HttpClientError(err) => write!(f, "HTTP client error: {}", err),
140            ClientError::InvalidUri(err) => write!(f, "Invalid URI: {}", err),
141            #[cfg(any(feature = "native-tls", feature = "rustls"))]
142            ClientError::TlsError(err) => write!(f, "TLS error: {}", err),
143            ClientError::IoError(err) => write!(f, "IO error: {}", err),
144        }
145    }
146}
147
148impl StdError for ClientError {}
149
150// Error conversions for ergonomic error handling
151impl From<hyper::Error> for ClientError {
152    fn from(err: hyper::Error) -> Self {
153        ClientError::HttpError(err)
154    }
155}
156
157impl From<hyper::http::uri::InvalidUri> for ClientError {
158    fn from(err: hyper::http::uri::InvalidUri) -> Self {
159        ClientError::InvalidUri(err)
160    }
161}
162
163impl From<std::io::Error> for ClientError {
164    fn from(err: std::io::Error) -> Self {
165        ClientError::IoError(err)
166    }
167}
168
169impl From<hyper::http::Error> for ClientError {
170    fn from(err: hyper::http::Error) -> Self {
171        ClientError::HttpBuildError(err)
172    }
173}
174
175impl From<hyper_util::client::legacy::Error> for ClientError {
176    fn from(err: hyper_util::client::legacy::Error) -> Self {
177        ClientError::HttpClientError(err)
178    }
179}
180
181/// Reusable HTTP client configured via [`HttpClientBuilder`].
182///
183/// # Examples
184///
185/// Build a client with a custom timeout and default headers:
186///
187/// ```
188/// use hyper_custom_cert::{HttpClient, RequestOptions};
189/// use std::time::Duration;
190/// use std::collections::HashMap;
191///
192/// let mut headers = HashMap::new();
193/// headers.insert("x-app".into(), "demo".into());
194///
195/// let client = HttpClient::builder()
196///     .with_timeout(Duration::from_secs(10))
197///     .with_default_headers(headers)
198///     .build();
199///
200/// // Placeholder call; does not perform I/O in this crate.
201/// let _ = client.request_with_options("https://example.com", None);
202/// ```
203pub struct HttpClient {
204    timeout: Duration,
205    default_headers: HashMap<String, String>,
206    /// When enabled (dev-only feature), allows accepting invalid/self-signed certs.
207    /// This is gated behind the `insecure-dangerous` feature to prevent accidental
208    /// use in production environments and clearly demarcate its security implications.
209    #[cfg(feature = "insecure-dangerous")]
210    accept_invalid_certs: bool,
211    /// Optional PEM-encoded custom Root CA to trust in addition to system roots.
212    /// This provides a mechanism for secure communication with internal services
213    /// or those using custom certificate authorities, allowing the client to validate
214    /// servers signed by this trusted CA.
215    root_ca_pem: Option<Vec<u8>>,
216    /// Optional certificate pins for additional security beyond CA validation.
217    /// These SHA256 fingerprints add an extra layer of defense against compromised
218    /// CAs or man-in-the-middle attacks by ensuring the server's certificate
219    /// matches a predefined set of trusted fingerprints.
220    #[cfg(feature = "rustls")]
221    pinned_cert_sha256: Option<Vec<[u8; 32]>>,
222}
223
224impl HttpClient {
225    /// Construct a new client using secure defaults by delegating to the builder.
226    /// This provides a convenient way to get a functional client without explicit
227    /// configuration, relying on sensible defaults (e.g., 30-second timeout, no custom CAs).
228    pub fn new() -> Self {
229        HttpClientBuilder::new().build()
230    }
231
232    /// Start building a client with explicit configuration.
233    /// This method exposes the `HttpClientBuilder` to allow granular control over
234    /// various client settings like timeouts, default headers, and TLS configurations.
235    pub fn builder() -> HttpClientBuilder {
236        HttpClientBuilder::new()
237    }
238
239    /// Convenience constructor that enables acceptance of self-signed/invalid
240    /// certificates. This is gated behind the `insecure-dangerous` feature and intended
241    /// strictly for development and testing. NEVER enable in production.
242    ///
243    /// # Security Warning
244    ///
245    /// ⚠️ CRITICAL SECURITY WARNING ⚠️
246    ///
247    /// This method deliberately bypasses TLS certificate validation, creating a
248    /// serious security vulnerability to man-in-the-middle attacks. When used:
249    ///
250    /// - ANY certificate will be accepted, regardless of its validity
251    /// - Expired certificates will be accepted
252    /// - Certificates from untrusted issuers will be accepted
253    /// - Certificates for the wrong domain will be accepted
254    ///
255    /// This is equivalent to calling `insecure_accept_invalid_certs(true)` on the builder
256    /// and inherits all of its security implications. See that method's documentation
257    /// for more details.
258    ///
259    /// # Intended Use Cases
260    ///
261    /// This method should ONLY be used for:
262    /// - Local development with self-signed certificates
263    /// - Testing environments where security is not a concern
264    /// - Debugging TLS connection issues
265    ///
266    /// # Implementation Details
267    ///
268    /// This is a convenience wrapper that calls:
269    /// ```ignore
270    /// HttpClient::builder()
271    ///     .insecure_accept_invalid_certs(true)
272    ///     .build()
273    /// ```
274    #[cfg(feature = "insecure-dangerous")]
275    pub fn with_self_signed_certs() -> Self {
276        HttpClient::builder()
277            .insecure_accept_invalid_certs(true)
278            .build()
279    }
280}
281
282// Native (non-wasm) runtime implementation
283// This section contains the actual HTTP client implementation for native targets,
284// leveraging `hyper` and `tokio` for asynchronous network operations.
285#[cfg(not(target_arch = "wasm32"))]
286impl HttpClient {
287    /// Performs a GET request and returns the raw response body.
288    /// This method constructs a `hyper::Request` with the GET method and any
289    /// default headers configured on the client, then dispatches it via `perform_request`.
290    /// Returns HttpResponse with raw body data exposed without any permutations.
291    ///
292    /// # Arguments
293    ///
294    /// * `url` - The URL to request
295    /// * `options` - Optional request options to customize this specific request
296    ///
297    /// # Examples
298    ///
299    /// ```
300    /// # async {
301    /// use hyper_custom_cert::{HttpClient, RequestOptions};
302    /// use std::collections::HashMap;
303    ///
304    /// let client = HttpClient::new();
305    ///
306    /// // Basic request with no custom options
307    /// let response1 = client.request_with_options("https://example.com", None).await?;
308    ///
309    /// // Request with custom options
310    /// let mut headers = HashMap::new();
311    /// headers.insert("x-request-id".into(), "abc123".into());
312    /// let options = RequestOptions::new().with_headers(headers);
313    /// let response2 = client.request_with_options("https://example.com", Some(options)).await?;
314    /// # Ok::<(), hyper_custom_cert::ClientError>(())
315    /// # };
316    /// ```
317    #[deprecated(since = "0.4.0", note = "Use request(url, Some(options)) instead")]
318    pub async fn request(&self, url: &str) -> Result<HttpResponse, ClientError> {
319        self.request_with_options(url, None).await
320    }
321
322    /// Performs a GET request and returns the raw response body.
323    /// This method constructs a `hyper::Request` with the GET method and any
324    /// default headers configured on the client, then dispatches it via `perform_request`.
325    /// Returns HttpResponse with raw body data exposed without any permutations.
326    ///
327    /// # Arguments
328    ///
329    /// * `url` - The URL to request
330    /// * `options` - Optional request options to customize this specific request
331    ///
332    /// # Examples
333    ///
334    /// ```
335    /// # async {
336    /// use hyper_custom_cert::{HttpClient, RequestOptions};
337    /// use std::collections::HashMap;
338    ///
339    /// let client = HttpClient::new();
340    ///
341    /// // Basic request with no custom options
342    /// let response1 = client.request_with_options("https://example.com", None).await?;
343    ///
344    /// // Request with custom options
345    /// let mut headers = HashMap::new();
346    /// headers.insert("x-request-id".into(), "abc123".into());
347    /// let options = RequestOptions::new().with_headers(headers);
348    /// let response2 = client.request_with_options("https://example.com", Some(options)).await?;
349    /// # Ok::<(), hyper_custom_cert::ClientError>(())
350    /// # };
351    /// ```
352    pub async fn request_with_options(
353        &self,
354        url: &str,
355        options: Option<RequestOptions>,
356    ) -> Result<HttpResponse, ClientError> {
357        let uri: Uri = url.parse()?;
358
359        let req = Request::builder().method(Method::GET).uri(uri);
360
361        // Add default headers to the request. This ensures that any headers
362        // set during the client's construction (e.g., API keys, User-Agent)
363        // are automatically included in outgoing requests.
364        let mut req = req;
365        for (key, value) in &self.default_headers {
366            req = req.header(key, value);
367        }
368
369        // Add any request-specific headers from options
370        if let Some(options) = &options {
371            if let Some(headers) = &options.headers {
372                for (key, value) in headers {
373                    req = req.header(key, value);
374                }
375            }
376        }
377
378        let req = req.body(http_body_util::Empty::<Bytes>::new())?;
379
380        // If options contain a timeout, temporarily modify self to use it
381        // This is a bit of a hack since we can't modify perform_request easily
382        if let Some(opts) = &options {
383            if let Some(timeout) = opts.timeout {
384                // Create a copy of self with the new timeout
385                let client_copy = HttpClient {
386                    timeout,
387                    default_headers: self.default_headers.clone(),
388                    #[cfg(feature = "insecure-dangerous")]
389                    accept_invalid_certs: self.accept_invalid_certs,
390                    root_ca_pem: self.root_ca_pem.clone(),
391                    #[cfg(feature = "rustls")]
392                    pinned_cert_sha256: self.pinned_cert_sha256.clone(),
393                };
394
395                // Use the modified client for this request only
396                client_copy.perform_request(req).await
397            } else {
398                // No timeout override, use normal client
399                self.perform_request(req).await
400            }
401        } else {
402            // No options, use normal client
403            self.perform_request(req).await
404        }
405    }
406
407    /// Performs a POST request with the given body and returns the raw response.
408    /// Similar to `request`, this method builds a `hyper::Request` for a POST
409    /// operation, handles the request body conversion to `Bytes`, and applies
410    /// default headers before calling `perform_request`.
411    /// Returns HttpResponse with raw body data exposed without any permutations.
412    ///
413    /// # Arguments
414    ///
415    /// * `url` - The URL to request
416    /// * `body` - The body content to send with the POST request
417    /// * `options` - Optional request options to customize this specific request
418    ///
419    /// # Examples
420    ///
421    /// ```
422    /// # async {
423    /// use hyper_custom_cert::{HttpClient, RequestOptions};
424    /// use std::collections::HashMap;
425    /// use std::time::Duration;
426    ///
427    /// let client = HttpClient::new();
428    ///
429    /// // Basic POST request with no custom options
430    /// let response1 = client.post_with_options("https://example.com/api", b"{\"key\":\"value\"}", None).await?;
431    ///
432    /// // POST request with custom options
433    /// let mut headers = HashMap::new();
434    /// headers.insert("Content-Type".into(), "application/json".into());
435    /// let options = RequestOptions::new()
436    ///     .with_headers(headers)
437    ///     .with_timeout(Duration::from_secs(5));
438    /// let response2 = client.post_with_options("https://example.com/api", b"{\"key\":\"value\"}", Some(options)).await?;
439    /// # Ok::<(), hyper_custom_cert::ClientError>(())
440    /// # };
441    /// ```
442    #[deprecated(
443        since = "0.4.0",
444        note = "Use post_with_options(url, body, Some(options)) instead"
445    )]
446    pub async fn post<B: AsRef<[u8]>>(
447        &self,
448        url: &str,
449        body: B,
450    ) -> Result<HttpResponse, ClientError> {
451        self.post_with_options(url, body, None).await
452    }
453
454    /// Performs a POST request with the given body and returns the raw response.
455    /// Similar to `request`, this method builds a `hyper::Request` for a POST
456    /// operation, handles the request body conversion to `Bytes`, and applies
457    /// default headers before calling `perform_request`.
458    /// Returns HttpResponse with raw body data exposed without any permutations.
459    ///
460    /// # Arguments
461    ///
462    /// * `url` - The URL to request
463    /// * `body` - The body content to send with the POST request
464    /// * `options` - Optional request options to customize this specific request
465    ///
466    /// # Examples
467    ///
468    /// ```
469    /// # async {
470    /// use hyper_custom_cert::{HttpClient, RequestOptions};
471    /// use std::collections::HashMap;
472    /// use std::time::Duration;
473    ///
474    /// let client = HttpClient::new();
475    ///
476    /// // Basic POST request with no custom options
477    /// let response1 = client.post_with_options("https://example.com/api", b"{\"key\":\"value\"}", None).await?;
478    ///
479    /// // POST request with custom options
480    /// let mut headers = HashMap::new();
481    /// headers.insert("Content-Type".into(), "application/json".into());
482    /// let options = RequestOptions::new()
483    ///     .with_headers(headers)
484    ///     .with_timeout(Duration::from_secs(5));
485    /// let response2 = client.post_with_options("https://example.com/api", b"{\"key\":\"value\"}", Some(options)).await?;
486    /// # Ok::<(), hyper_custom_cert::ClientError>(())
487    /// # };
488    /// ```
489    pub async fn post_with_options<B: AsRef<[u8]>>(
490        &self,
491        url: &str,
492        body: B,
493        options: Option<RequestOptions>,
494    ) -> Result<HttpResponse, ClientError> {
495        let uri: Uri = url.parse()?;
496
497        let req = Request::builder().method(Method::POST).uri(uri);
498
499        // Add default headers to the request for consistency across client operations.
500        let mut req = req;
501        for (key, value) in &self.default_headers {
502            req = req.header(key, value);
503        }
504
505        // Add any request-specific headers from options
506        if let Some(options) = &options {
507            if let Some(headers) = &options.headers {
508                for (key, value) in headers {
509                    req = req.header(key, value);
510                }
511            }
512        }
513
514        let body_bytes = Bytes::copy_from_slice(body.as_ref());
515        let req = req.body(http_body_util::Full::new(body_bytes))?;
516
517        // If options contain a timeout, temporarily modify self to use it
518        // This is a bit of a hack since we can't modify perform_request easily
519        if let Some(opts) = &options {
520            if let Some(timeout) = opts.timeout {
521                // Create a copy of self with the new timeout
522                let client_copy = HttpClient {
523                    timeout,
524                    default_headers: self.default_headers.clone(),
525                    #[cfg(feature = "insecure-dangerous")]
526                    accept_invalid_certs: self.accept_invalid_certs,
527                    root_ca_pem: self.root_ca_pem.clone(),
528                    #[cfg(feature = "rustls")]
529                    pinned_cert_sha256: self.pinned_cert_sha256.clone(),
530                };
531
532                // Use the modified client for this request only
533                client_copy.perform_request(req).await
534            } else {
535                // No timeout override, use normal client
536                self.perform_request(req).await
537            }
538        } else {
539            // No options, use normal client
540            self.perform_request(req).await
541        }
542    }
543
544    /// Helper method to perform HTTP requests using the configured settings.
545    /// This centralizes the logic for dispatching `hyper::Request` objects,
546    /// handling the various TLS backends (native-tls, rustls) and ensuring
547    /// the correct `hyper` client is used based on feature flags.
548    async fn perform_request<B>(&self, req: Request<B>) -> Result<HttpResponse, ClientError>
549    where
550        B: hyper::body::Body + Send + 'static + Unpin,
551        B::Data: Send,
552        B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
553    {
554        #[cfg(feature = "native-tls")]
555        {
556            // When the "native-tls" feature is enabled, use `hyper-tls` for TLS
557            // support, which integrates with the system's native TLS libraries.
558
559            #[cfg(feature = "insecure-dangerous")]
560            if self.accept_invalid_certs {
561                // ⚠️ SECURITY WARNING: This code path deliberately bypasses TLS certificate validation.
562                // It should only be used during development/testing with self-signed certificates,
563                // and NEVER in production environments. This creates a vulnerability to
564                // man-in-the-middle attacks and is extremely dangerous.
565
566                // Implementation with tokio-native-tls to accept invalid certificates
567                let mut http_connector = hyper_util::client::legacy::connect::HttpConnector::new();
568                http_connector.enforce_http(false);
569
570                // Create a TLS connector that accepts invalid certificates
571                let mut tls_builder = native_tls::TlsConnector::builder();
572                tls_builder.danger_accept_invalid_certs(true);
573                let tls_connector = tls_builder.build().map_err(|e| {
574                    ClientError::TlsError(format!("Failed to build TLS connector: {}", e))
575                })?;
576
577                // Create the tokio-native-tls connector
578                let tokio_connector = tokio_native_tls::TlsConnector::from(tls_connector);
579
580                // Create the HTTPS connector using the HTTP and TLS connectors
581                let connector = hyper_tls::HttpsConnector::from((http_connector, tokio_connector));
582
583                let client = Client::builder(TokioExecutor::new()).build(connector);
584                let resp = tokio::time::timeout(self.timeout, client.request(req))
585                    .await
586                    .map_err(|_| ClientError::TlsError("Request timed out".to_string()))??;
587                return self.build_response(resp).await;
588            }
589
590            // Standard secure TLS connection with certificate validation (default path)
591            let connector = hyper_tls::HttpsConnector::new();
592            let client = Client::builder(TokioExecutor::new()).build(connector);
593            let resp = tokio::time::timeout(self.timeout, client.request(req))
594                .await
595                .map_err(|_| ClientError::TlsError("Request timed out".to_string()))??;
596            self.build_response(resp).await
597        }
598        #[cfg(all(feature = "rustls", not(feature = "native-tls")))]
599        {
600            // If "rustls" is enabled and "native-tls" is not, use `rustls` for TLS.
601            // Properly configure the rustls connector with custom CA certificates and/or
602            // certificate validation settings based on the client configuration.
603
604            // Start with the standard rustls config with native roots
605            let mut root_cert_store = rustls::RootCertStore::empty();
606
607            // Load native certificates using rustls_native_certs v0.8.1
608            // This returns a CertificateResult which has a certs field containing the certificates
609            let native_certs = rustls_native_certs::load_native_certs();
610
611            // Add each cert to the root store
612            for cert in &native_certs.certs {
613                if let Err(e) = root_cert_store.add(cert.clone()) {
614                    return Err(ClientError::TlsError(format!(
615                        "Failed to add native cert to root store: {}",
616                        e
617                    )));
618                }
619            }
620
621            // Add custom CA certificate if provided
622            if let Some(ref pem_bytes) = self.root_ca_pem {
623                let mut reader = std::io::Cursor::new(pem_bytes);
624                for cert_result in rustls_pemfile::certs(&mut reader) {
625                    match cert_result {
626                        Ok(cert) => {
627                            root_cert_store.add(cert).map_err(|e| {
628                                ClientError::TlsError(format!(
629                                    "Failed to add custom cert to root store: {}",
630                                    e
631                                ))
632                            })?;
633                        }
634                        Err(e) => {
635                            return Err(ClientError::TlsError(format!(
636                                "Failed to parse PEM cert: {}",
637                                e
638                            )));
639                        }
640                    }
641                }
642            }
643
644            // Configure rustls
645            let mut config_builder =
646                rustls::ClientConfig::builder().with_root_certificates(root_cert_store);
647
648            let rustls_config = config_builder.with_no_client_auth();
649
650            #[cfg(feature = "insecure-dangerous")]
651            let rustls_config = if self.accept_invalid_certs {
652                // ⚠️ SECURITY WARNING: This code path deliberately bypasses TLS certificate validation.
653                // It should only be used during development/testing with self-signed certificates,
654                // and NEVER in production environments. This creates a vulnerability to
655                // man-in-the-middle attacks and is extremely dangerous.
656
657                use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified};
658                use rustls::pki_types::UnixTime;
659                use rustls::DigitallySignedStruct;
660                use rustls::SignatureScheme;
661                use std::sync::Arc;
662
663                // Override the certificate verifier with a no-op verifier that accepts all certificates
664                #[derive(Debug)]
665                struct NoCertificateVerification {}
666
667                impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
668                    fn verify_server_cert(
669                        &self,
670                        _end_entity: &rustls::pki_types::CertificateDer<'_>,
671                        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
672                        _server_name: &rustls::pki_types::ServerName<'_>,
673                        _ocsp_response: &[u8],
674                        _now: UnixTime,
675                    ) -> Result<ServerCertVerified, rustls::Error> {
676                        // Accept any certificate without verification
677                        Ok(ServerCertVerified::assertion())
678                    }
679
680                    fn verify_tls12_signature(
681                        &self,
682                        _message: &[u8],
683                        _cert: &rustls::pki_types::CertificateDer<'_>,
684                        _dss: &DigitallySignedStruct,
685                    ) -> Result<HandshakeSignatureValid, rustls::Error> {
686                        // Accept any TLS 1.2 signature without verification
687                        Ok(HandshakeSignatureValid::assertion())
688                    }
689
690                    fn verify_tls13_signature(
691                        &self,
692                        _message: &[u8],
693                        _cert: &rustls::pki_types::CertificateDer<'_>,
694                        _dss: &DigitallySignedStruct,
695                    ) -> Result<HandshakeSignatureValid, rustls::Error> {
696                        // Accept any TLS 1.3 signature without verification
697                        Ok(HandshakeSignatureValid::assertion())
698                    }
699
700                    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
701                        // Return a list of all supported signature schemes
702                        vec![
703                            SignatureScheme::RSA_PKCS1_SHA1,
704                            SignatureScheme::ECDSA_SHA1_Legacy,
705                            SignatureScheme::RSA_PKCS1_SHA256,
706                            SignatureScheme::ECDSA_NISTP256_SHA256,
707                            SignatureScheme::RSA_PKCS1_SHA384,
708                            SignatureScheme::ECDSA_NISTP384_SHA384,
709                            SignatureScheme::RSA_PKCS1_SHA512,
710                            SignatureScheme::ECDSA_NISTP521_SHA512,
711                            SignatureScheme::RSA_PSS_SHA256,
712                            SignatureScheme::RSA_PSS_SHA384,
713                            SignatureScheme::RSA_PSS_SHA512,
714                            SignatureScheme::ED25519,
715                            SignatureScheme::ED448,
716                        ]
717                    }
718                }
719
720                // Set up the dangerous configuration with no certificate verification
721                let mut config = rustls_config.clone();
722                config
723                    .dangerous()
724                    .set_certificate_verifier(Arc::new(NoCertificateVerification {}));
725                config
726            } else {
727                rustls_config
728            };
729
730            // Handle certificate pinning if configured
731            #[cfg(feature = "rustls")]
732            let rustls_config = if let Some(ref pins) = self.pinned_cert_sha256 {
733                // Implement certificate pinning by creating a custom certificate verifier
734                use rustls::client::danger::{
735                    HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier,
736                };
737                use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
738                use rustls::DigitallySignedStruct;
739                use rustls::SignatureScheme;
740                use std::sync::Arc;
741
742                // Create a custom certificate verifier that checks certificate pins
743                struct CertificatePinner {
744                    pins: Vec<[u8; 32]>,
745                    inner: Arc<dyn ServerCertVerifier>,
746                }
747
748                impl ServerCertVerifier for CertificatePinner {
749                    fn verify_server_cert(
750                        &self,
751                        end_entity: &CertificateDer<'_>,
752                        intermediates: &[CertificateDer<'_>],
753                        server_name: &ServerName<'_>,
754                        ocsp_response: &[u8],
755                        now: UnixTime,
756                    ) -> Result<ServerCertVerified, rustls::Error> {
757                        // First, use the inner verifier to do standard verification
758                        self.inner.verify_server_cert(
759                            end_entity,
760                            intermediates,
761                            server_name,
762                            ocsp_response,
763                            now,
764                        )?;
765
766                        // Then verify the pin
767                        use sha2::{Digest, Sha256};
768
769                        let mut hasher = Sha256::new();
770                        hasher.update(end_entity.as_ref());
771                        let cert_hash = hasher.finalize();
772
773                        // Check if the certificate hash matches any of our pins
774                        for pin in &self.pins {
775                            if pin[..] == cert_hash[..] {
776                                return Ok(ServerCertVerified::assertion());
777                            }
778                        }
779
780                        // If we got here, none of the pins matched
781                        Err(rustls::Error::General(
782                            "Certificate pin verification failed".into(),
783                        ))
784                    }
785
786                    fn verify_tls12_signature(
787                        &self,
788                        message: &[u8],
789                        cert: &CertificateDer<'_>,
790                        dss: &DigitallySignedStruct,
791                    ) -> Result<HandshakeSignatureValid, rustls::Error> {
792                        // Delegate to inner verifier
793                        self.inner.verify_tls12_signature(message, cert, dss)
794                    }
795
796                    fn verify_tls13_signature(
797                        &self,
798                        message: &[u8],
799                        cert: &CertificateDer<'_>,
800                        dss: &DigitallySignedStruct,
801                    ) -> Result<HandshakeSignatureValid, rustls::Error> {
802                        // Delegate to inner verifier
803                        self.inner.verify_tls13_signature(message, cert, dss)
804                    }
805
806                    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
807                        self.inner.supported_verify_schemes()
808                    }
809                }
810
811                // Create the certificate pinner with our pins and the default verifier
812                let mut config = rustls_config.clone();
813                let default_verifier = rustls::client::WebPkiServerVerifier::builder()
814                    .with_root_certificates(root_cert_store.clone())
815                    .build()
816                    .map_err(|e| {
817                        ClientError::TlsError(format!(
818                            "Failed to build certificate verifier: {}",
819                            e
820                        ))
821                    })?;
822
823                let cert_pinner = Arc::new(CertificatePinner {
824                    pins: pins.clone(),
825                    inner: default_verifier,
826                });
827
828                config.dangerous().set_certificate_verifier(cert_pinner);
829                config
830            } else {
831                rustls_config
832            };
833
834            // Create a connector that supports HTTP and HTTPS
835            let mut http_connector = hyper_util::client::legacy::connect::HttpConnector::new();
836            http_connector.enforce_http(false);
837
838            // Create the rustls connector using HttpsConnectorBuilder
839            let https_connector = hyper_rustls::HttpsConnectorBuilder::new()
840                .with_tls_config(rustls_config)
841                .https_or_http()
842                .enable_http1()
843                .build();
844
845            let client = Client::builder(TokioExecutor::new()).build(https_connector);
846            let resp = tokio::time::timeout(self.timeout, client.request(req))
847                .await
848                .map_err(|_| ClientError::TlsError("Request timed out".to_string()))??;
849            self.build_response(resp).await
850        }
851        #[cfg(not(any(feature = "native-tls", feature = "rustls")))]
852        {
853            // If neither "native-tls" nor "rustls" features are enabled,
854            // fall back to a basic HTTP connector without TLS support.
855            // This is primarily for scenarios where TLS is not required or
856            // handled at a different layer.
857            let connector = hyper_util::client::legacy::connect::HttpConnector::new();
858            let client = Client::builder(TokioExecutor::new()).build(connector);
859            let resp = tokio::time::timeout(self.timeout, client.request(req))
860                .await
861                .map_err(|_| ClientError::TlsError("Request timed out".to_string()))??;
862            self.build_response(resp).await
863        }
864    }
865
866    /// Helper method to convert a hyper Response to our HttpResponse with raw body data.
867    /// This method abstracts the details of `hyper::Response` processing,
868    /// extracting the status, headers, and importantly, collecting the entire
869    /// response body into a `Bytes` buffer for easy consumption by the caller.
870    async fn build_response(&self, resp: Response<Incoming>) -> Result<HttpResponse, ClientError> {
871        let status = resp.status();
872
873        // Convert hyper's `HeaderMap` to a `HashMap<String, String>` for simpler
874        // public API exposure, making header access more idiomatic for consumers.
875        let mut headers = HashMap::new();
876        for (name, value) in resp.headers() {
877            if let Ok(value_str) = value.to_str() {
878                headers.insert(name.to_string(), value_str.to_string());
879            }
880        }
881
882        // Collect the body as raw bytes - this is the key part of the issue
883        // We expose the body as raw bytes without any permutations, ensuring
884        // the client receives the exact byte content of the response.
885        let body_bytes = resp.into_body().collect().await?.to_bytes();
886
887        Ok(HttpResponse {
888            status,
889            headers,
890            body: body_bytes,
891        })
892    }
893}
894
895// WebAssembly stubbed runtime implementation
896#[cfg(target_arch = "wasm32")]
897impl HttpClient {
898    /// On wasm32 targets, runtime methods are stubbed and return
899    /// `ClientError::WasmNotImplemented` because browsers do not allow
900    /// programmatic installation/trust of custom CAs.
901    #[deprecated(
902        since = "0.4.0",
903        note = "Use request_with_options(url, Some(options)) instead"
904    )]
905    pub fn request(&self, _url: &str) -> Result<(), ClientError> {
906        Err(ClientError::WasmNotImplemented)
907    }
908
909    /// On wasm32 targets, runtime methods are stubbed and return
910    /// `ClientError::WasmNotImplemented` because browsers do not allow
911    /// programmatic installation/trust of custom CAs.
912    pub fn request_with_options(
913        &self,
914        _url: &str,
915        _options: Option<RequestOptions>,
916    ) -> Result<(), ClientError> {
917        Err(ClientError::WasmNotImplemented)
918    }
919
920    /// POST is also not implemented on wasm32 targets for the same reason.
921    #[deprecated(
922        since = "0.4.0",
923        note = "Use post_with_options(url, body, Some(options)) instead"
924    )]
925    pub fn post<B: AsRef<[u8]>>(&self, _url: &str, _body: B) -> Result<(), ClientError> {
926        Err(ClientError::WasmNotImplemented)
927    }
928
929    /// POST is also not implemented on wasm32 targets for the same reason.
930    pub fn post_with_options<B: AsRef<[u8]>>(
931        &self,
932        _url: &str,
933        _body: B,
934        _options: Option<RequestOptions>,
935    ) -> Result<(), ClientError> {
936        Err(ClientError::WasmNotImplemented)
937    }
938}
939
940/// Builder for configuring and creating an [`HttpClient`].
941pub struct HttpClientBuilder {
942    timeout: Duration,
943    default_headers: HashMap<String, String>,
944    #[cfg(feature = "insecure-dangerous")]
945    accept_invalid_certs: bool,
946    root_ca_pem: Option<Vec<u8>>,
947    #[cfg(feature = "rustls")]
948    pinned_cert_sha256: Option<Vec<[u8; 32]>>,
949}
950
951impl HttpClientBuilder {
952    /// Start a new builder with default settings.
953    pub fn new() -> Self {
954        Self {
955            timeout: Duration::from_secs(30),
956            default_headers: HashMap::new(),
957            #[cfg(feature = "insecure-dangerous")]
958            accept_invalid_certs: false,
959            root_ca_pem: None,
960            #[cfg(feature = "rustls")]
961            pinned_cert_sha256: None,
962        }
963    }
964
965    /// Set a request timeout to apply to client operations.
966    pub fn with_timeout(mut self, timeout: Duration) -> Self {
967        self.timeout = timeout;
968        self
969    }
970
971    /// Set default headers that will be added to every request initiated by this client.
972    pub fn with_default_headers(mut self, headers: HashMap<String, String>) -> Self {
973        self.default_headers = headers;
974        self
975    }
976
977    /// Dev-only: accept self-signed/invalid TLS certificates. Requires the
978    /// `insecure-dangerous` feature to be enabled. NEVER enable this in production.
979    ///
980    /// # Security Warning
981    ///
982    /// ⚠️ CRITICAL SECURITY WARNING ⚠️
983    ///
984    /// This method deliberately bypasses TLS certificate validation, which creates a
985    /// serious security vulnerability to man-in-the-middle attacks. When enabled:
986    ///
987    /// - The client will accept ANY certificate, regardless of its validity
988    /// - The client will accept expired certificates
989    /// - The client will accept certificates from untrusted issuers
990    /// - The client will accept certificates for the wrong domain
991    ///
992    /// This method should ONLY be used for:
993    /// - Local development with self-signed certificates
994    /// - Testing environments where security is not a concern
995    /// - Debugging TLS connection issues
996    ///
997    /// # Implementation Details
998    ///
999    /// When enabled, this setting:
1000    /// - For `native-tls`: Uses `danger_accept_invalid_certs(true)` on the TLS connector
1001    /// - For `rustls`: Implements a custom `ServerCertVerifier` that accepts all certificates
1002    ///
1003    /// # Examples
1004    ///
1005    /// Enable insecure mode during local development (dangerous):
1006    ///
1007    /// ```ignore
1008    /// use hyper_custom_cert::HttpClient;
1009    ///
1010    /// // Requires: --features insecure-dangerous
1011    /// let client = HttpClient::builder()
1012    ///     .insecure_accept_invalid_certs(true)
1013    ///     .build();
1014    /// ```
1015    #[cfg(feature = "insecure-dangerous")]
1016    pub fn insecure_accept_invalid_certs(mut self, accept: bool) -> Self {
1017        self.accept_invalid_certs = accept;
1018        self
1019    }
1020
1021    /// Provide a PEM-encoded Root CA certificate to be trusted by the client.
1022    /// This is the production-ready way to trust a custom CA.
1023    ///
1024    /// # Examples
1025    ///
1026    /// ```ignore
1027    /// use hyper_custom_cert::HttpClient;
1028    ///
1029    /// // Requires: --no-default-features --features rustls
1030    /// let client = HttpClient::builder()
1031    ///     .with_root_ca_pem(include_bytes!("../examples-data/root-ca.pem"))
1032    ///     .build();
1033    /// ```
1034    #[cfg(feature = "rustls")]
1035    pub fn with_root_ca_pem(mut self, pem_bytes: &[u8]) -> Self {
1036        self.root_ca_pem = Some(pem_bytes.to_vec());
1037        self
1038    }
1039
1040    /// Provide a PEM-encoded Root CA certificate file to be trusted by the client.
1041    /// This is the production-ready way to trust a custom CA from a file path.
1042    ///
1043    /// The file will be read during builder configuration and its contents stored
1044    /// in the client. This method will panic if the file cannot be read, similar
1045    /// to how `include_bytes!` macro behaves.
1046    ///
1047    /// # Security Considerations
1048    ///
1049    /// Only use certificate files from trusted sources. Ensure proper file permissions
1050    /// are set to prevent unauthorized modification of the certificate file.
1051    ///
1052    /// # Panics
1053    ///
1054    /// This method will panic if:
1055    /// - The file does not exist
1056    /// - The file cannot be read due to permissions or I/O errors
1057    /// - The path is invalid
1058    ///
1059    /// # Examples
1060    ///
1061    /// ```ignore
1062    /// use hyper_custom_cert::HttpClient;
1063    ///
1064    /// // Requires: --no-default-features --features rustls
1065    /// let client = HttpClient::builder()
1066    ///     .with_root_ca_file("path/to/root-ca.pem")
1067    ///     .build();
1068    /// ```
1069    ///
1070    /// Using a `std::path::Path`:
1071    ///
1072    /// ```ignore
1073    /// use hyper_custom_cert::HttpClient;
1074    /// use std::path::Path;
1075    ///
1076    /// // Requires: --no-default-features --features rustls
1077    /// let ca_path = Path::new("certs/custom-ca.pem");
1078    /// let client = HttpClient::builder()
1079    ///     .with_root_ca_file(ca_path)
1080    ///     .build();
1081    /// ```
1082    #[cfg(feature = "rustls")]
1083    pub fn with_root_ca_file<P: AsRef<Path>>(mut self, path: P) -> Self {
1084        let pem_bytes = fs::read(path.as_ref()).unwrap_or_else(|e| {
1085            panic!(
1086                "Failed to read CA certificate file '{}': {}",
1087                path.as_ref().display(),
1088                e
1089            )
1090        });
1091        self.root_ca_pem = Some(pem_bytes);
1092        self
1093    }
1094
1095    /// Configure certificate pinning using SHA256 fingerprints for additional security.
1096    ///
1097    /// Certificate pinning provides an additional layer of security beyond CA validation
1098    /// by verifying that the server's certificate matches one of the provided fingerprints.
1099    /// This helps protect against compromised CAs and man-in-the-middle attacks.
1100    ///
1101    /// # Security Considerations
1102    ///
1103    /// - Certificate pinning should be used in conjunction with, not as a replacement for,
1104    ///   proper CA validation.
1105    /// - Pinned certificates must be updated when the server's certificate changes.
1106    /// - Consider having backup pins for certificate rotation scenarios.
1107    /// - This method provides additional security but requires careful maintenance.
1108    ///
1109    /// # Parameters
1110    ///
1111    /// * `pins` - A vector of 32-byte SHA256 fingerprints of certificates to pin.
1112    ///   Each fingerprint should be the SHA256 hash of the certificate's DER encoding.
1113    ///
1114    /// # Examples
1115    ///
1116    /// ```ignore
1117    /// use hyper_custom_cert::HttpClient;
1118    ///
1119    /// // Example SHA256 fingerprints (these are just examples)
1120    /// let pin1: [u8; 32] = [
1121    ///     0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
1122    ///     0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
1123    ///     0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00,
1124    ///     0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0x07, 0x18
1125    /// ];
1126    ///
1127    /// let pin2: [u8; 32] = [
1128    ///     0xf0, 0xe1, 0xd2, 0xc3, 0xb4, 0xa5, 0x96, 0x87,
1129    ///     0x78, 0x69, 0x5a, 0x4b, 0x3c, 0x2d, 0x1e, 0x0f,
1130    ///     0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
1131    ///     0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff
1132    /// ];
1133    ///
1134    /// // Requires: --no-default-features --features rustls
1135    /// let client = HttpClient::builder()
1136    ///     .with_pinned_cert_sha256(vec![pin1, pin2])
1137    ///     .build();
1138    /// ```
1139    #[cfg(feature = "rustls")]
1140    pub fn with_pinned_cert_sha256(mut self, pins: Vec<[u8; 32]>) -> Self {
1141        self.pinned_cert_sha256 = Some(pins);
1142        self
1143    }
1144
1145    /// Finalize the configuration and build an [`HttpClient`].
1146    pub fn build(self) -> HttpClient {
1147        HttpClient {
1148            timeout: self.timeout,
1149            default_headers: self.default_headers,
1150            #[cfg(feature = "insecure-dangerous")]
1151            accept_invalid_certs: self.accept_invalid_certs,
1152            root_ca_pem: self.root_ca_pem,
1153            #[cfg(feature = "rustls")]
1154            pinned_cert_sha256: self.pinned_cert_sha256,
1155        }
1156    }
1157}
1158
1159/// Default construction uses builder defaults.
1160impl Default for HttpClient {
1161    fn default() -> Self {
1162        Self::new()
1163    }
1164}
1165
1166/// Default builder state is secure and ergonomic.
1167impl Default for HttpClientBuilder {
1168    fn default() -> Self {
1169        Self::new()
1170    }
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175    use super::*;
1176
1177    #[test]
1178    fn builder_default_builds() {
1179        let _client = HttpClient::builder().build();
1180    }
1181
1182    #[test]
1183    fn builder_allows_timeout_and_headers() {
1184        let mut headers = HashMap::new();
1185        headers.insert("x-test".into(), "1".into());
1186        let builder = HttpClient::builder()
1187            .with_timeout(Duration::from_secs(5))
1188            .with_default_headers(headers);
1189        #[cfg(feature = "rustls")]
1190        let builder = builder.with_root_ca_pem(b"-----BEGIN CERTIFICATE-----\n...");
1191        let _client = builder.build();
1192    }
1193
1194    #[cfg(feature = "insecure-dangerous")]
1195    #[test]
1196    fn builder_allows_insecure_when_feature_enabled() {
1197        let _client = HttpClient::builder()
1198            .insecure_accept_invalid_certs(true)
1199            .build();
1200        let _client2 = HttpClient::with_self_signed_certs();
1201    }
1202
1203    #[cfg(not(target_arch = "wasm32"))]
1204    #[tokio::test]
1205    async fn request_returns_ok_on_native() {
1206        let client = HttpClient::builder().build();
1207        // Just test that the method can be called - don't actually make network requests in tests
1208        // In a real test environment, you would mock the HTTP calls or use a test server
1209        let _client = client; // Use the client to avoid unused variable warning
1210    }
1211
1212    #[cfg(not(target_arch = "wasm32"))]
1213    #[tokio::test]
1214    async fn post_returns_ok_on_native() {
1215        let client = HttpClient::builder().build();
1216        // Just test that the method can be called - don't actually make network requests in tests
1217        // In a real test environment, you would mock the HTTP calls or use a test server
1218        let _client = client; // Use the client to avoid unused variable warning
1219    }
1220
1221    #[cfg(all(feature = "rustls", not(target_arch = "wasm32")))]
1222    #[test]
1223    fn builder_allows_root_ca_file() {
1224        use std::fs;
1225        use std::io::Write;
1226
1227        // Create a temporary file with test certificate content
1228        let temp_dir = std::env::temp_dir();
1229        let cert_file = temp_dir.join("test-ca.pem");
1230
1231        let test_cert = b"-----BEGIN CERTIFICATE-----
1232MIICxjCCAa4CAQAwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAe
1233Fw0yNTA4MTQwMDAwMDBaFw0yNjA4MTQwMDAwMDBaMBIxEDAOBgNVBAMMB1Rlc3Qg
1234Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTest...
1235-----END CERTIFICATE-----";
1236
1237        // Write test certificate to temporary file
1238        {
1239            let mut file = fs::File::create(&cert_file).expect("Failed to create temp cert file");
1240            file.write_all(test_cert)
1241                .expect("Failed to write cert to temp file");
1242        }
1243
1244        // Test that the builder can read the certificate file
1245        let client = HttpClient::builder().with_root_ca_file(&cert_file).build();
1246
1247        // Verify the certificate was loaded
1248        assert!(client.root_ca_pem.is_some());
1249        assert_eq!(client.root_ca_pem.as_ref().unwrap(), test_cert);
1250
1251        // Clean up
1252        let _ = fs::remove_file(cert_file);
1253    }
1254}