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 hyper::{body::Incoming, Request, Response, StatusCode, Uri, Method};
40use hyper_util::client::legacy::Client;
41use hyper_util::rt::TokioExecutor;
42use http_body_util::BodyExt;
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(&self, url: &str, options: Option<RequestOptions>) -> Result<HttpResponse, ClientError> {
353        let uri: Uri = url.parse()?;
354        
355        let req = Request::builder()
356            .method(Method::GET)
357            .uri(uri);
358        
359        // Add default headers to the request. This ensures that any headers
360        // set during the client's construction (e.g., API keys, User-Agent)
361        // are automatically included in outgoing requests.
362        let mut req = req;
363        for (key, value) in &self.default_headers {
364            req = req.header(key, value);
365        }
366        
367        // Add any request-specific headers from options
368        if let Some(options) = &options {
369            if let Some(headers) = &options.headers {
370                for (key, value) in headers {
371                    req = req.header(key, value);
372                }
373            }
374        }
375        
376        let req = req.body(http_body_util::Empty::<Bytes>::new())?;
377        
378        // If options contain a timeout, temporarily modify self to use it
379        // This is a bit of a hack since we can't modify perform_request easily
380        let result = if let Some(opts) = &options {
381            if let Some(timeout) = opts.timeout {
382                // Create a copy of self with the new timeout
383                let client_copy = HttpClient {
384                    timeout,
385                    default_headers: self.default_headers.clone(),
386                    #[cfg(feature = "insecure-dangerous")]
387                    accept_invalid_certs: self.accept_invalid_certs,
388                    root_ca_pem: self.root_ca_pem.clone(),
389                    #[cfg(feature = "rustls")]
390                    pinned_cert_sha256: self.pinned_cert_sha256.clone(),
391                };
392                
393                // Use the modified client for this request only
394                client_copy.perform_request(req).await
395            } else {
396                // No timeout override, use normal client
397                self.perform_request(req).await
398            }
399        } else {
400            // No options, use normal client
401            self.perform_request(req).await
402        };
403        
404        result
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(since = "0.4.0", note = "Use post_with_options(url, body, Some(options)) instead")]
443    pub async fn post<B: AsRef<[u8]>>(&self, url: &str, body: B) -> Result<HttpResponse, ClientError> {
444        self.post_with_options(url, body, None).await
445    }
446    
447    /// Performs a POST request with the given body and returns the raw response.
448    /// Similar to `request`, this method builds a `hyper::Request` for a POST
449    /// operation, handles the request body conversion to `Bytes`, and applies
450    /// default headers before calling `perform_request`.
451    /// Returns HttpResponse with raw body data exposed without any permutations.
452    ///
453    /// # Arguments
454    ///
455    /// * `url` - The URL to request
456    /// * `body` - The body content to send with the POST request
457    /// * `options` - Optional request options to customize this specific request
458    ///
459    /// # Examples
460    ///
461    /// ```
462    /// # async {
463    /// use hyper_custom_cert::{HttpClient, RequestOptions};
464    /// use std::collections::HashMap;
465    /// use std::time::Duration;
466    /// 
467    /// let client = HttpClient::new();
468    /// 
469    /// // Basic POST request with no custom options
470    /// let response1 = client.post_with_options("https://example.com/api", b"{\"key\":\"value\"}", None).await?;
471    /// 
472    /// // POST request with custom options
473    /// let mut headers = HashMap::new();
474    /// headers.insert("Content-Type".into(), "application/json".into());
475    /// let options = RequestOptions::new()
476    ///     .with_headers(headers)
477    ///     .with_timeout(Duration::from_secs(5));
478    /// let response2 = client.post_with_options("https://example.com/api", b"{\"key\":\"value\"}", Some(options)).await?;
479    /// # Ok::<(), hyper_custom_cert::ClientError>(())
480    /// # };
481    /// ```
482    pub async fn post_with_options<B: AsRef<[u8]>>(&self, url: &str, body: B, options: Option<RequestOptions>) -> Result<HttpResponse, ClientError> {
483        let uri: Uri = url.parse()?;
484        
485        let req = Request::builder()
486            .method(Method::POST)
487            .uri(uri);
488        
489        // Add default headers to the request for consistency across client operations.
490        let mut req = req;
491        for (key, value) in &self.default_headers {
492            req = req.header(key, value);
493        }
494        
495        // Add any request-specific headers from options
496        if let Some(options) = &options {
497            if let Some(headers) = &options.headers {
498                for (key, value) in headers {
499                    req = req.header(key, value);
500                }
501            }
502        }
503        
504        let body_bytes = Bytes::copy_from_slice(body.as_ref());
505        let req = req.body(http_body_util::Full::new(body_bytes))?;
506        
507        // If options contain a timeout, temporarily modify self to use it
508        // This is a bit of a hack since we can't modify perform_request easily
509        let result = if let Some(opts) = &options {
510            if let Some(timeout) = opts.timeout {
511                // Create a copy of self with the new timeout
512                let client_copy = HttpClient {
513                    timeout,
514                    default_headers: self.default_headers.clone(),
515                    #[cfg(feature = "insecure-dangerous")]
516                    accept_invalid_certs: self.accept_invalid_certs,
517                    root_ca_pem: self.root_ca_pem.clone(),
518                    #[cfg(feature = "rustls")]
519                    pinned_cert_sha256: self.pinned_cert_sha256.clone(),
520                };
521                
522                // Use the modified client for this request only
523                client_copy.perform_request(req).await
524            } else {
525                // No timeout override, use normal client
526                self.perform_request(req).await
527            }
528        } else {
529            // No options, use normal client
530            self.perform_request(req).await
531        };
532        
533        result
534    }
535
536    /// Helper method to perform HTTP requests using the configured settings.
537    /// This centralizes the logic for dispatching `hyper::Request` objects,
538    /// handling the various TLS backends (native-tls, rustls) and ensuring
539    /// the correct `hyper` client is used based on feature flags.
540    async fn perform_request<B>(&self, req: Request<B>) -> Result<HttpResponse, ClientError> 
541    where
542        B: hyper::body::Body + Send + 'static + Unpin,
543        B::Data: Send,
544        B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
545    {
546        #[cfg(feature = "native-tls")]
547        {
548            // When the "native-tls" feature is enabled, use `hyper-tls` for TLS
549            // support, which integrates with the system's native TLS libraries.
550            
551            #[cfg(feature = "insecure-dangerous")]
552            if self.accept_invalid_certs {
553                // ⚠️ SECURITY WARNING: This code path deliberately bypasses TLS certificate validation.
554                // It should only be used during development/testing with self-signed certificates,
555                // and NEVER in production environments. This creates a vulnerability to 
556                // man-in-the-middle attacks and is extremely dangerous.
557                
558                // Implementation with tokio-native-tls to accept invalid certificates
559                let mut http_connector = hyper_util::client::legacy::connect::HttpConnector::new();
560                http_connector.enforce_http(false);
561                
562                // Create a TLS connector that accepts invalid certificates
563                let mut tls_builder = native_tls::TlsConnector::builder();
564                tls_builder.danger_accept_invalid_certs(true);
565                let tls_connector = tls_builder.build()
566                    .map_err(|e| ClientError::TlsError(format!("Failed to build TLS connector: {}", e)))?;
567                
568                // Create the tokio-native-tls connector
569                let tokio_connector = tokio_native_tls::TlsConnector::from(tls_connector);
570                
571                // Create the HTTPS connector using the HTTP and TLS connectors
572                let connector = hyper_tls::HttpsConnector::from((http_connector, tokio_connector));
573                
574                let client = Client::builder(TokioExecutor::new())
575                    .build(connector);
576                let resp = tokio::time::timeout(self.timeout, client.request(req))
577                    .await
578                    .map_err(|_| ClientError::TlsError("Request timed out".to_string()))?
579                    ?;
580                return self.build_response(resp).await;
581            }
582            
583            // Standard secure TLS connection with certificate validation (default path)
584            let connector = hyper_tls::HttpsConnector::new();
585            let client = Client::builder(TokioExecutor::new()).build(connector);
586            let resp = tokio::time::timeout(self.timeout, client.request(req))
587                .await
588                .map_err(|_| ClientError::TlsError("Request timed out".to_string()))?
589                ?;
590            self.build_response(resp).await
591        }
592        #[cfg(all(feature = "rustls", not(feature = "native-tls")))]
593        {
594            // If "rustls" is enabled and "native-tls" is not, use `rustls` for TLS.
595            // Properly configure the rustls connector with custom CA certificates and/or
596            // certificate validation settings based on the client configuration.
597            
598            // Start with the standard rustls config with native roots
599            let mut root_cert_store = rustls::RootCertStore::empty();
600            
601            // Load native certificates using rustls_native_certs v0.8.1
602            // This returns a CertificateResult which has a certs field containing the certificates
603            let native_certs = rustls_native_certs::load_native_certs();
604            
605            // Add each cert to the root store
606            for cert in &native_certs.certs {
607                if let Err(e) = root_cert_store.add(cert.clone()) {
608                    return Err(ClientError::TlsError(format!("Failed to add native cert to root store: {}", e)));
609                }
610            }
611            
612            // Add custom CA certificate if provided
613            if let Some(ref pem_bytes) = self.root_ca_pem {
614                let mut reader = std::io::Cursor::new(pem_bytes);
615                for cert_result in rustls_pemfile::certs(&mut reader) {
616                    match cert_result {
617                        Ok(cert) => {
618                            root_cert_store.add(cert)
619                                .map_err(|e| ClientError::TlsError(format!("Failed to add custom cert to root store: {}", e)))?;
620                        },
621                        Err(e) => return Err(ClientError::TlsError(format!("Failed to parse PEM cert: {}", e))),
622                    }
623                }
624            }
625            
626            // Configure rustls
627            let mut config_builder = rustls::ClientConfig::builder()
628                .with_root_certificates(root_cert_store);
629            
630            let rustls_config = config_builder.with_no_client_auth();
631            
632            #[cfg(feature = "insecure-dangerous")]
633            let rustls_config = if self.accept_invalid_certs {
634                // ⚠️ SECURITY WARNING: This code path deliberately bypasses TLS certificate validation.
635                // It should only be used during development/testing with self-signed certificates,
636                // and NEVER in production environments. This creates a vulnerability to 
637                // man-in-the-middle attacks and is extremely dangerous.
638                
639                use std::sync::Arc;
640                use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified};
641                use rustls::DigitallySignedStruct;
642                use rustls::SignatureScheme;
643                use rustls::pki_types::UnixTime;
644                
645                // Override the certificate verifier with a no-op verifier that accepts all certificates
646                #[derive(Debug)]
647                struct NoCertificateVerification {}
648                
649                impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
650                    fn verify_server_cert(
651                        &self,
652                        _end_entity: &rustls::pki_types::CertificateDer<'_>,
653                        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
654                        _server_name: &rustls::pki_types::ServerName<'_>,
655                        _ocsp_response: &[u8],
656                        _now: UnixTime,
657                    ) -> Result<ServerCertVerified, rustls::Error> {
658                        // Accept any certificate without verification
659                        Ok(ServerCertVerified::assertion())
660                    }
661                    
662                    fn verify_tls12_signature(
663                        &self,
664                        _message: &[u8],
665                        _cert: &rustls::pki_types::CertificateDer<'_>,
666                        _dss: &DigitallySignedStruct,
667                    ) -> Result<HandshakeSignatureValid, rustls::Error> {
668                        // Accept any TLS 1.2 signature without verification
669                        Ok(HandshakeSignatureValid::assertion())
670                    }
671                    
672                    fn verify_tls13_signature(
673                        &self,
674                        _message: &[u8],
675                        _cert: &rustls::pki_types::CertificateDer<'_>,
676                        _dss: &DigitallySignedStruct,
677                    ) -> Result<HandshakeSignatureValid, rustls::Error> {
678                        // Accept any TLS 1.3 signature without verification
679                        Ok(HandshakeSignatureValid::assertion())
680                    }
681                    
682                    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
683                        // Return a list of all supported signature schemes
684                        vec![
685                            SignatureScheme::RSA_PKCS1_SHA1,
686                            SignatureScheme::ECDSA_SHA1_Legacy,
687                            SignatureScheme::RSA_PKCS1_SHA256,
688                            SignatureScheme::ECDSA_NISTP256_SHA256,
689                            SignatureScheme::RSA_PKCS1_SHA384,
690                            SignatureScheme::ECDSA_NISTP384_SHA384,
691                            SignatureScheme::RSA_PKCS1_SHA512,
692                            SignatureScheme::ECDSA_NISTP521_SHA512,
693                            SignatureScheme::RSA_PSS_SHA256,
694                            SignatureScheme::RSA_PSS_SHA384,
695                            SignatureScheme::RSA_PSS_SHA512,
696                            SignatureScheme::ED25519,
697                            SignatureScheme::ED448,
698                        ]
699                    }
700                }
701                
702                // Set up the dangerous configuration with no certificate verification
703                let mut config = rustls_config.clone();
704                config.dangerous().set_certificate_verifier(Arc::new(NoCertificateVerification {}));
705                config
706            } else {
707                rustls_config
708            };
709            
710            // Handle certificate pinning if configured
711            #[cfg(feature = "rustls")]
712            let rustls_config = if let Some(ref pins) = self.pinned_cert_sha256 {
713                // Implement certificate pinning by creating a custom certificate verifier
714                use std::sync::Arc;
715                use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
716                use rustls::DigitallySignedStruct;
717                use rustls::SignatureScheme;
718                use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
719                
720                // Create a custom certificate verifier that checks certificate pins
721                struct CertificatePinner {
722                    pins: Vec<[u8; 32]>,
723                    inner: Arc<dyn ServerCertVerifier>,
724                }
725                
726                impl ServerCertVerifier for CertificatePinner {
727                    fn verify_server_cert(
728                        &self,
729                        end_entity: &CertificateDer<'_>,
730                        intermediates: &[CertificateDer<'_>],
731                        server_name: &ServerName<'_>,
732                        ocsp_response: &[u8],
733                        now: UnixTime,
734                    ) -> Result<ServerCertVerified, rustls::Error> {
735                        // First, use the inner verifier to do standard verification
736                        self.inner.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)?;
737                        
738                        // Then verify the pin
739                        use sha2::{Sha256, Digest};
740                        
741                        let mut hasher = Sha256::new();
742                        hasher.update(end_entity.as_ref());
743                        let cert_hash = hasher.finalize();
744                        
745                        // Check if the certificate hash matches any of our pins
746                        for pin in &self.pins {
747                            if pin[..] == cert_hash[..] {
748                                return Ok(ServerCertVerified::assertion());
749                            }
750                        }
751                        
752                        // If we got here, none of the pins matched
753                        Err(rustls::Error::General("Certificate pin verification failed".into()))
754                    }
755                    
756                    fn verify_tls12_signature(
757                        &self, 
758                        message: &[u8],
759                        cert: &CertificateDer<'_>,
760                        dss: &DigitallySignedStruct,
761                    ) -> Result<HandshakeSignatureValid, rustls::Error> {
762                        // Delegate to inner verifier
763                        self.inner.verify_tls12_signature(message, cert, dss)
764                    }
765                    
766                    fn verify_tls13_signature(
767                        &self,
768                        message: &[u8],
769                        cert: &CertificateDer<'_>,
770                        dss: &DigitallySignedStruct,
771                    ) -> Result<HandshakeSignatureValid, rustls::Error> {
772                        // Delegate to inner verifier
773                        self.inner.verify_tls13_signature(message, cert, dss)
774                    }
775                    
776                    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
777                        self.inner.supported_verify_schemes()
778                    }
779                }
780                
781                // Create the certificate pinner with our pins and the default verifier
782                let mut config = rustls_config.clone();
783                let default_verifier = rustls::client::WebPkiServerVerifier::builder()
784                    .with_root_certificates(root_cert_store.clone())
785                    .build()
786                    .map_err(|e| ClientError::TlsError(format!("Failed to build certificate verifier: {}", e)))?;
787                
788                let cert_pinner = Arc::new(CertificatePinner {
789                    pins: pins.clone(),
790                    inner: default_verifier,
791                });
792                
793                config.dangerous().set_certificate_verifier(cert_pinner);
794                config
795            } else {
796                rustls_config
797            };
798            
799            // Create a connector that supports HTTP and HTTPS
800            let mut http_connector = hyper_util::client::legacy::connect::HttpConnector::new();
801            http_connector.enforce_http(false);
802            
803            // Create the rustls connector using HttpsConnectorBuilder
804            let https_connector = hyper_rustls::HttpsConnectorBuilder::new()
805                .with_tls_config(rustls_config)
806                .https_or_http()
807                .enable_http1()
808                .build();
809            
810            let client = Client::builder(TokioExecutor::new()).build(https_connector);
811            let resp = tokio::time::timeout(self.timeout, client.request(req))
812                .await
813                .map_err(|_| ClientError::TlsError("Request timed out".to_string()))?
814                ?;
815            self.build_response(resp).await
816        }
817        #[cfg(not(any(feature = "native-tls", feature = "rustls")))]
818        {
819            // If neither "native-tls" nor "rustls" features are enabled,
820            // fall back to a basic HTTP connector without TLS support.
821            // This is primarily for scenarios where TLS is not required or
822            // handled at a different layer.
823            let connector = hyper_util::client::legacy::connect::HttpConnector::new();
824            let client = Client::builder(TokioExecutor::new()).build(connector);
825            let resp = tokio::time::timeout(self.timeout, client.request(req))
826                .await
827                .map_err(|_| ClientError::TlsError("Request timed out".to_string()))?
828                ?;
829            self.build_response(resp).await
830        }
831    }
832
833    /// Helper method to convert a hyper Response to our HttpResponse with raw body data.
834    /// This method abstracts the details of `hyper::Response` processing,
835    /// extracting the status, headers, and importantly, collecting the entire
836    /// response body into a `Bytes` buffer for easy consumption by the caller.
837    async fn build_response(&self, resp: Response<Incoming>) -> Result<HttpResponse, ClientError> {
838        let status = resp.status();
839        
840        // Convert hyper's `HeaderMap` to a `HashMap<String, String>` for simpler
841        // public API exposure, making header access more idiomatic for consumers.
842        let mut headers = HashMap::new();
843        for (name, value) in resp.headers() {
844            if let Ok(value_str) = value.to_str() {
845                headers.insert(name.to_string(), value_str.to_string());
846            }
847        }
848        
849        // Collect the body as raw bytes - this is the key part of the issue
850        // We expose the body as raw bytes without any permutations, ensuring
851        // the client receives the exact byte content of the response.
852        let body_bytes = resp.into_body().collect().await?.to_bytes();
853        
854        Ok(HttpResponse {
855            status,
856            headers,
857            body: body_bytes,
858        })
859    }
860}
861
862// WebAssembly stubbed runtime implementation
863#[cfg(target_arch = "wasm32")]
864impl HttpClient {
865    /// On wasm32 targets, runtime methods are stubbed and return
866    /// `ClientError::WasmNotImplemented` because browsers do not allow
867    /// programmatic installation/trust of custom CAs.
868    #[deprecated(since = "0.4.0", note = "Use request_with_options(url, Some(options)) instead")]
869    pub fn request(&self, _url: &str) -> Result<(), ClientError> {
870        Err(ClientError::WasmNotImplemented)
871    }
872    
873    /// On wasm32 targets, runtime methods are stubbed and return
874    /// `ClientError::WasmNotImplemented` because browsers do not allow
875    /// programmatic installation/trust of custom CAs.
876    pub fn request_with_options(&self, _url: &str, _options: Option<RequestOptions>) -> Result<(), ClientError> {
877        Err(ClientError::WasmNotImplemented)
878    }
879
880    /// POST is also not implemented on wasm32 targets for the same reason.
881    #[deprecated(since = "0.4.0", note = "Use post_with_options(url, body, Some(options)) instead")]
882    pub fn post<B: AsRef<[u8]>>(&self, _url: &str, _body: B) -> Result<(), ClientError> {
883        Err(ClientError::WasmNotImplemented)
884    }
885    
886    /// POST is also not implemented on wasm32 targets for the same reason.
887    pub fn post_with_options<B: AsRef<[u8]>>(&self, _url: &str, _body: B, _options: Option<RequestOptions>) -> Result<(), ClientError> {
888        Err(ClientError::WasmNotImplemented)
889    }
890}
891
892/// Builder for configuring and creating an [`HttpClient`].
893pub struct HttpClientBuilder {
894    timeout: Duration,
895    default_headers: HashMap<String, String>,
896    #[cfg(feature = "insecure-dangerous")]
897    accept_invalid_certs: bool,
898    root_ca_pem: Option<Vec<u8>>,
899    #[cfg(feature = "rustls")]
900    pinned_cert_sha256: Option<Vec<[u8; 32]>>,
901}
902
903impl HttpClientBuilder {
904    /// Start a new builder with default settings.
905    pub fn new() -> Self {
906        Self {
907            timeout: Duration::from_secs(30),
908            default_headers: HashMap::new(),
909            #[cfg(feature = "insecure-dangerous")]
910            accept_invalid_certs: false,
911            root_ca_pem: None,
912            #[cfg(feature = "rustls")]
913            pinned_cert_sha256: None,
914        }
915    }
916
917    /// Set a request timeout to apply to client operations.
918    pub fn with_timeout(mut self, timeout: Duration) -> Self {
919        self.timeout = timeout;
920        self
921    }
922
923    /// Set default headers that will be added to every request initiated by this client.
924    pub fn with_default_headers(mut self, headers: HashMap<String, String>) -> Self {
925        self.default_headers = headers;
926        self
927    }
928
929    /// Dev-only: accept self-signed/invalid TLS certificates. Requires the
930    /// `insecure-dangerous` feature to be enabled. NEVER enable this in production.
931    /// 
932    /// # Security Warning
933    /// 
934    /// ⚠️ CRITICAL SECURITY WARNING ⚠️
935    /// 
936    /// This method deliberately bypasses TLS certificate validation, which creates a
937    /// serious security vulnerability to man-in-the-middle attacks. When enabled:
938    /// 
939    /// - The client will accept ANY certificate, regardless of its validity
940    /// - The client will accept expired certificates
941    /// - The client will accept certificates from untrusted issuers
942    /// - The client will accept certificates for the wrong domain
943    /// 
944    /// This method should ONLY be used for:
945    /// - Local development with self-signed certificates
946    /// - Testing environments where security is not a concern
947    /// - Debugging TLS connection issues
948    /// 
949    /// # Implementation Details
950    /// 
951    /// When enabled, this setting:
952    /// - For `native-tls`: Uses `danger_accept_invalid_certs(true)` on the TLS connector
953    /// - For `rustls`: Implements a custom `ServerCertVerifier` that accepts all certificates
954    ///
955    /// # Examples
956    ///
957    /// Enable insecure mode during local development (dangerous):
958    ///
959    /// ```ignore
960    /// use hyper_custom_cert::HttpClient;
961    ///
962    /// // Requires: --features insecure-dangerous
963    /// let client = HttpClient::builder()
964    ///     .insecure_accept_invalid_certs(true)
965    ///     .build();
966    /// ```
967    #[cfg(feature = "insecure-dangerous")]
968    pub fn insecure_accept_invalid_certs(mut self, accept: bool) -> Self {
969        self.accept_invalid_certs = accept;
970        self
971    }
972
973    /// Provide a PEM-encoded Root CA certificate to be trusted by the client.
974    /// This is the production-ready way to trust a custom CA.
975    ///
976    /// # Examples
977    ///
978    /// ```ignore
979    /// use hyper_custom_cert::HttpClient;
980    ///
981    /// // Requires: --no-default-features --features rustls
982    /// let client = HttpClient::builder()
983    ///     .with_root_ca_pem(include_bytes!("../examples-data/root-ca.pem"))
984    ///     .build();
985    /// ```
986    #[cfg(feature = "rustls")]
987    pub fn with_root_ca_pem(mut self, pem_bytes: &[u8]) -> Self {
988        self.root_ca_pem = Some(pem_bytes.to_vec());
989        self
990    }
991
992    /// Provide a PEM-encoded Root CA certificate file to be trusted by the client.
993    /// This is the production-ready way to trust a custom CA from a file path.
994    ///
995    /// The file will be read during builder configuration and its contents stored
996    /// in the client. This method will panic if the file cannot be read, similar
997    /// to how `include_bytes!` macro behaves.
998    ///
999    /// # Security Considerations
1000    ///
1001    /// Only use certificate files from trusted sources. Ensure proper file permissions
1002    /// are set to prevent unauthorized modification of the certificate file.
1003    ///
1004    /// # Panics
1005    ///
1006    /// This method will panic if:
1007    /// - The file does not exist
1008    /// - The file cannot be read due to permissions or I/O errors
1009    /// - The path is invalid
1010    ///
1011    /// # Examples
1012    ///
1013    /// ```ignore
1014    /// use hyper_custom_cert::HttpClient;
1015    ///
1016    /// // Requires: --no-default-features --features rustls
1017    /// let client = HttpClient::builder()
1018    ///     .with_root_ca_file("path/to/root-ca.pem")
1019    ///     .build();
1020    /// ```
1021    ///
1022    /// Using a `std::path::Path`:
1023    ///
1024    /// ```ignore
1025    /// use hyper_custom_cert::HttpClient;
1026    /// use std::path::Path;
1027    ///
1028    /// // Requires: --no-default-features --features rustls
1029    /// let ca_path = Path::new("certs/custom-ca.pem");
1030    /// let client = HttpClient::builder()
1031    ///     .with_root_ca_file(ca_path)
1032    ///     .build();
1033    /// ```
1034    #[cfg(feature = "rustls")]
1035    pub fn with_root_ca_file<P: AsRef<Path>>(mut self, path: P) -> Self {
1036        let pem_bytes = fs::read(path.as_ref()).unwrap_or_else(|e| {
1037            panic!(
1038                "Failed to read CA certificate file '{}': {}",
1039                path.as_ref().display(),
1040                e
1041            )
1042        });
1043        self.root_ca_pem = Some(pem_bytes);
1044        self
1045    }
1046
1047    /// Configure certificate pinning using SHA256 fingerprints for additional security.
1048    ///
1049    /// Certificate pinning provides an additional layer of security beyond CA validation
1050    /// by verifying that the server's certificate matches one of the provided fingerprints.
1051    /// This helps protect against compromised CAs and man-in-the-middle attacks.
1052    ///
1053    /// # Security Considerations
1054    ///
1055    /// - Certificate pinning should be used in conjunction with, not as a replacement for,
1056    ///   proper CA validation.
1057    /// - Pinned certificates must be updated when the server's certificate changes.
1058    /// - Consider having backup pins for certificate rotation scenarios.
1059    /// - This method provides additional security but requires careful maintenance.
1060    ///
1061    /// # Parameters
1062    ///
1063    /// * `pins` - A vector of 32-byte SHA256 fingerprints of certificates to pin.
1064    ///   Each fingerprint should be the SHA256 hash of the certificate's DER encoding.
1065    ///
1066    /// # Examples
1067    ///
1068    /// ```ignore
1069    /// use hyper_custom_cert::HttpClient;
1070    ///
1071    /// // Example SHA256 fingerprints (these are just examples)
1072    /// let pin1: [u8; 32] = [
1073    ///     0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
1074    ///     0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
1075    ///     0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00,
1076    ///     0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0x07, 0x18
1077    /// ];
1078    ///
1079    /// let pin2: [u8; 32] = [
1080    ///     0xf0, 0xe1, 0xd2, 0xc3, 0xb4, 0xa5, 0x96, 0x87,
1081    ///     0x78, 0x69, 0x5a, 0x4b, 0x3c, 0x2d, 0x1e, 0x0f,
1082    ///     0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
1083    ///     0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff
1084    /// ];
1085    ///
1086    /// // Requires: --no-default-features --features rustls
1087    /// let client = HttpClient::builder()
1088    ///     .with_pinned_cert_sha256(vec![pin1, pin2])
1089    ///     .build();
1090    /// ```
1091    #[cfg(feature = "rustls")]
1092    pub fn with_pinned_cert_sha256(mut self, pins: Vec<[u8; 32]>) -> Self {
1093        self.pinned_cert_sha256 = Some(pins);
1094        self
1095    }
1096
1097    /// Finalize the configuration and build an [`HttpClient`].
1098    pub fn build(self) -> HttpClient {
1099        HttpClient {
1100            timeout: self.timeout,
1101            default_headers: self.default_headers,
1102            #[cfg(feature = "insecure-dangerous")]
1103            accept_invalid_certs: self.accept_invalid_certs,
1104            root_ca_pem: self.root_ca_pem,
1105            #[cfg(feature = "rustls")]
1106            pinned_cert_sha256: self.pinned_cert_sha256,
1107        }
1108    }
1109}
1110
1111/// Default construction uses builder defaults.
1112impl Default for HttpClient {
1113    fn default() -> Self {
1114        Self::new()
1115    }
1116}
1117
1118/// Default builder state is secure and ergonomic.
1119impl Default for HttpClientBuilder {
1120    fn default() -> Self {
1121        Self::new()
1122    }
1123}
1124
1125#[cfg(test)]
1126mod tests {
1127    use super::*;
1128
1129    #[test]
1130    fn builder_default_builds() {
1131        let _client = HttpClient::builder().build();
1132    }
1133
1134    #[test]
1135    fn builder_allows_timeout_and_headers() {
1136        let mut headers = HashMap::new();
1137        headers.insert("x-test".into(), "1".into());
1138        let builder = HttpClient::builder()
1139            .with_timeout(Duration::from_secs(5))
1140            .with_default_headers(headers);
1141        #[cfg(feature = "rustls")]
1142        let builder = builder.with_root_ca_pem(b"-----BEGIN CERTIFICATE-----\n...");
1143        let _client = builder.build();
1144    }
1145
1146    #[cfg(feature = "insecure-dangerous")]
1147    #[test]
1148    fn builder_allows_insecure_when_feature_enabled() {
1149        let _client = HttpClient::builder()
1150            .insecure_accept_invalid_certs(true)
1151            .build();
1152        let _client2 = HttpClient::with_self_signed_certs();
1153    }
1154
1155    #[cfg(not(target_arch = "wasm32"))]
1156    #[tokio::test]
1157    async fn request_returns_ok_on_native() {
1158        let client = HttpClient::builder().build();
1159        // Just test that the method can be called - don't actually make network requests in tests
1160        // In a real test environment, you would mock the HTTP calls or use a test server
1161        let _client = client; // Use the client to avoid unused variable warning
1162    }
1163
1164    #[cfg(not(target_arch = "wasm32"))]
1165    #[tokio::test]  
1166    async fn post_returns_ok_on_native() {
1167        let client = HttpClient::builder().build();
1168        // Just test that the method can be called - don't actually make network requests in tests
1169        // In a real test environment, you would mock the HTTP calls or use a test server
1170        let _client = client; // Use the client to avoid unused variable warning
1171    }
1172
1173    #[cfg(all(feature = "rustls", not(target_arch = "wasm32")))]
1174    #[test]
1175    fn builder_allows_root_ca_file() {
1176        use std::fs;
1177        use std::io::Write;
1178
1179        // Create a temporary file with test certificate content
1180        let temp_dir = std::env::temp_dir();
1181        let cert_file = temp_dir.join("test-ca.pem");
1182
1183        let test_cert = b"-----BEGIN CERTIFICATE-----
1184MIICxjCCAa4CAQAwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAe
1185Fw0yNTA4MTQwMDAwMDBaFw0yNjA4MTQwMDAwMDBaMBIxEDAOBgNVBAMMB1Rlc3Qg
1186Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTest...
1187-----END CERTIFICATE-----";
1188
1189        // Write test certificate to temporary file
1190        {
1191            let mut file = fs::File::create(&cert_file).expect("Failed to create temp cert file");
1192            file.write_all(test_cert)
1193                .expect("Failed to write cert to temp file");
1194        }
1195
1196        // Test that the builder can read the certificate file
1197        let client = HttpClient::builder().with_root_ca_file(&cert_file).build();
1198
1199        // Verify the certificate was loaded
1200        assert!(client.root_ca_pem.is_some());
1201        assert_eq!(client.root_ca_pem.as_ref().unwrap(), test_cert);
1202
1203        // Clean up
1204        let _ = fs::remove_file(cert_file);
1205    }
1206}