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/// HTTP response with raw body data exposed as bytes.
45#[derive(Debug, Clone)]
46pub struct HttpResponse {
47    /// HTTP status code
48    pub status: StatusCode,
49    /// Response headers
50    pub headers: HashMap<String, String>,
51    /// Raw response body as bytes - exposed without any permutations
52    pub body: Bytes,
53}
54
55/// Error type for this crate's runtime operations.
56#[derive(Debug)]
57pub enum ClientError {
58    /// Returned on wasm32 targets where runtime operations requiring custom CA
59    /// trust are not available due to browser security constraints.
60    WasmNotImplemented,
61    /// HTTP request failed
62    HttpError(hyper::Error),
63    /// HTTP request building failed
64    HttpBuildError(hyper::http::Error),
65    /// HTTP client request failed
66    HttpClientError(hyper_util::client::legacy::Error),
67    /// Invalid URI
68    InvalidUri(hyper::http::uri::InvalidUri),
69    /// TLS/Connection error
70    #[cfg(any(feature = "native-tls", feature = "rustls"))]
71    TlsError(String),
72    /// IO error (e.g., reading CA files)
73    IoError(std::io::Error),
74}
75
76impl fmt::Display for ClientError {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            ClientError::WasmNotImplemented => write!(
80                f,
81                "Not implemented on WebAssembly (browser restricts programmatic CA trust)"
82            ),
83            ClientError::HttpError(err) => write!(f, "HTTP error: {}", err),
84            ClientError::HttpBuildError(err) => write!(f, "HTTP build error: {}", err),
85            ClientError::HttpClientError(err) => write!(f, "HTTP client error: {}", err),
86            ClientError::InvalidUri(err) => write!(f, "Invalid URI: {}", err),
87            #[cfg(any(feature = "native-tls", feature = "rustls"))]
88            ClientError::TlsError(err) => write!(f, "TLS error: {}", err),
89            ClientError::IoError(err) => write!(f, "IO error: {}", err),
90        }
91    }
92}
93
94impl StdError for ClientError {}
95
96// Error conversions for ergonomic error handling
97impl From<hyper::Error> for ClientError {
98    fn from(err: hyper::Error) -> Self {
99        ClientError::HttpError(err)
100    }
101}
102
103impl From<hyper::http::uri::InvalidUri> for ClientError {
104    fn from(err: hyper::http::uri::InvalidUri) -> Self {
105        ClientError::InvalidUri(err)
106    }
107}
108
109impl From<std::io::Error> for ClientError {
110    fn from(err: std::io::Error) -> Self {
111        ClientError::IoError(err)
112    }
113}
114
115impl From<hyper::http::Error> for ClientError {
116    fn from(err: hyper::http::Error) -> Self {
117        ClientError::HttpBuildError(err)
118    }
119}
120
121impl From<hyper_util::client::legacy::Error> for ClientError {
122    fn from(err: hyper_util::client::legacy::Error) -> Self {
123        ClientError::HttpClientError(err)
124    }
125}
126
127/// Reusable HTTP client configured via [`HttpClientBuilder`].
128///
129/// # Examples
130///
131/// Build a client with a custom timeout and default headers:
132///
133/// ```
134/// use hyper_custom_cert::HttpClient;
135/// use std::time::Duration;
136/// use std::collections::HashMap;
137///
138/// let mut headers = HashMap::new();
139/// headers.insert("x-app".into(), "demo".into());
140///
141/// let client = HttpClient::builder()
142///     .with_timeout(Duration::from_secs(10))
143///     .with_default_headers(headers)
144///     .build();
145///
146/// // Placeholder call; does not perform I/O in this crate.
147/// let _ = client.request("https://example.com");
148/// ```
149pub struct HttpClient {
150    timeout: Duration,
151    default_headers: HashMap<String, String>,
152    /// When enabled (dev-only feature), allows accepting invalid/self-signed certs.
153    /// This is gated behind the `insecure-dangerous` feature to prevent accidental
154    /// use in production environments and clearly demarcate its security implications.
155    #[cfg(feature = "insecure-dangerous")]
156    accept_invalid_certs: bool,
157    /// Optional PEM-encoded custom Root CA to trust in addition to system roots.
158    /// This provides a mechanism for secure communication with internal services
159    /// or those using custom certificate authorities, allowing the client to validate
160    /// servers signed by this trusted CA.
161    root_ca_pem: Option<Vec<u8>>,
162    /// Optional certificate pins for additional security beyond CA validation.
163    /// These SHA256 fingerprints add an extra layer of defense against compromised
164    /// CAs or man-in-the-middle attacks by ensuring the server's certificate
165    /// matches a predefined set of trusted fingerprints.
166    #[cfg(feature = "rustls")]
167    pinned_cert_sha256: Option<Vec<[u8; 32]>>,
168}
169
170impl HttpClient {
171    /// Construct a new client using secure defaults by delegating to the builder.
172    /// This provides a convenient way to get a functional client without explicit
173    /// configuration, relying on sensible defaults (e.g., 30-second timeout, no custom CAs).
174    pub fn new() -> Self {
175        HttpClientBuilder::new().build()
176    }
177
178    /// Start building a client with explicit configuration.
179    /// This method exposes the `HttpClientBuilder` to allow granular control over
180    /// various client settings like timeouts, default headers, and TLS configurations.
181    pub fn builder() -> HttpClientBuilder {
182        HttpClientBuilder::new()
183    }
184
185    /// Convenience constructor that enables acceptance of self-signed/invalid
186    /// certificates. This is gated behind the `insecure-dangerous` feature and intended
187    /// strictly for development and testing. NEVER enable in production.
188    /// 
189    /// # Security Warning
190    /// 
191    /// ⚠️ CRITICAL SECURITY WARNING ⚠️
192    /// 
193    /// This method deliberately bypasses TLS certificate validation, creating a
194    /// serious security vulnerability to man-in-the-middle attacks. When used:
195    /// 
196    /// - ANY certificate will be accepted, regardless of its validity
197    /// - Expired certificates will be accepted
198    /// - Certificates from untrusted issuers will be accepted
199    /// - Certificates for the wrong domain will be accepted
200    /// 
201    /// This is equivalent to calling `insecure_accept_invalid_certs(true)` on the builder
202    /// and inherits all of its security implications. See that method's documentation
203    /// for more details.
204    /// 
205    /// # Intended Use Cases
206    /// 
207    /// This method should ONLY be used for:
208    /// - Local development with self-signed certificates
209    /// - Testing environments where security is not a concern
210    /// - Debugging TLS connection issues
211    /// 
212    /// # Implementation Details
213    /// 
214    /// This is a convenience wrapper that calls:
215    /// ```ignore
216    /// HttpClient::builder()
217    ///     .insecure_accept_invalid_certs(true)
218    ///     .build()
219    /// ```
220    #[cfg(feature = "insecure-dangerous")]
221    pub fn with_self_signed_certs() -> Self {
222        HttpClient::builder()
223            .insecure_accept_invalid_certs(true)
224            .build()
225    }
226}
227
228// Native (non-wasm) runtime implementation
229// This section contains the actual HTTP client implementation for native targets,
230// leveraging `hyper` and `tokio` for asynchronous network operations.
231#[cfg(not(target_arch = "wasm32"))]
232impl HttpClient {
233    /// Performs a GET request and returns the raw response body.
234    /// This method constructs a `hyper::Request` with the GET method and any
235    /// default headers configured on the client, then dispatches it via `perform_request`.
236    /// Returns HttpResponse with raw body data exposed without any permutations.
237    pub async fn request(&self, url: &str) -> Result<HttpResponse, ClientError> {
238        let uri: Uri = url.parse()?;
239        
240        let req = Request::builder()
241            .method(Method::GET)
242            .uri(uri);
243        
244        // Add default headers to the request. This ensures that any headers
245        // set during the client's construction (e.g., API keys, User-Agent)
246        // are automatically included in outgoing requests.
247        let mut req = req;
248        for (key, value) in &self.default_headers {
249            req = req.header(key, value);
250        }
251        
252        let req = req.body(http_body_util::Empty::<Bytes>::new())?;
253        
254        self.perform_request(req).await
255    }
256
257    /// Performs a POST request with the given body and returns the raw response.
258    /// Similar to `request`, this method builds a `hyper::Request` for a POST
259    /// operation, handles the request body conversion to `Bytes`, and applies
260    /// default headers before calling `perform_request`.
261    /// Returns HttpResponse with raw body data exposed without any permutations.
262    pub async fn post<B: AsRef<[u8]>>(&self, url: &str, body: B) -> Result<HttpResponse, ClientError> {
263        let uri: Uri = url.parse()?;
264        
265        let req = Request::builder()
266            .method(Method::POST)
267            .uri(uri);
268        
269        // Add default headers to the request for consistency across client operations.
270        let mut req = req;
271        for (key, value) in &self.default_headers {
272            req = req.header(key, value);
273        }
274        
275        let body_bytes = Bytes::copy_from_slice(body.as_ref());
276        let req = req.body(http_body_util::Full::new(body_bytes))?;
277        
278        self.perform_request(req).await
279    }
280
281    /// Helper method to perform HTTP requests using the configured settings.
282    /// This centralizes the logic for dispatching `hyper::Request` objects,
283    /// handling the various TLS backends (native-tls, rustls) and ensuring
284    /// the correct `hyper` client is used based on feature flags.
285    async fn perform_request<B>(&self, req: Request<B>) -> Result<HttpResponse, ClientError> 
286    where
287        B: hyper::body::Body + Send + 'static + Unpin,
288        B::Data: Send,
289        B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
290    {
291        #[cfg(feature = "native-tls")]
292        {
293            // When the "native-tls" feature is enabled, use `hyper-tls` for TLS
294            // support, which integrates with the system's native TLS libraries.
295            
296            #[cfg(feature = "insecure-dangerous")]
297            if self.accept_invalid_certs {
298                // ⚠️ SECURITY WARNING: This code path deliberately bypasses TLS certificate validation.
299                // It should only be used during development/testing with self-signed certificates,
300                // and NEVER in production environments. This creates a vulnerability to 
301                // man-in-the-middle attacks and is extremely dangerous.
302                
303                // Implementation with tokio-native-tls to accept invalid certificates
304                let mut http_connector = hyper_util::client::legacy::connect::HttpConnector::new();
305                http_connector.enforce_http(false);
306                
307                // Create a TLS connector that accepts invalid certificates
308                let mut tls_builder = native_tls::TlsConnector::builder();
309                tls_builder.danger_accept_invalid_certs(true);
310                let tls_connector = tls_builder.build()
311                    .map_err(|e| ClientError::TlsError(format!("Failed to build TLS connector: {}", e)))?;
312                
313                // Create the tokio-native-tls connector
314                let tokio_connector = tokio_native_tls::TlsConnector::from(tls_connector);
315                
316                // Create the HTTPS connector using the HTTP and TLS connectors
317                let connector = hyper_tls::HttpsConnector::from((http_connector, tokio_connector));
318                
319                let client = Client::builder(TokioExecutor::new())
320                    .build(connector);
321                let resp = tokio::time::timeout(self.timeout, client.request(req))
322                    .await
323                    .map_err(|_| ClientError::TlsError("Request timed out".to_string()))?
324                    ?;
325                return self.build_response(resp).await;
326            }
327            
328            // Standard secure TLS connection with certificate validation (default path)
329            let connector = hyper_tls::HttpsConnector::new();
330            let client = Client::builder(TokioExecutor::new()).build(connector);
331            let resp = tokio::time::timeout(self.timeout, client.request(req))
332                .await
333                .map_err(|_| ClientError::TlsError("Request timed out".to_string()))?
334                ?;
335            self.build_response(resp).await
336        }
337        #[cfg(all(feature = "rustls", not(feature = "native-tls")))]
338        {
339            // If "rustls" is enabled and "native-tls" is not, use `rustls` for TLS.
340            // Properly configure the rustls connector with custom CA certificates and/or
341            // certificate validation settings based on the client configuration.
342            
343            // Start with the standard rustls config with native roots
344            let mut root_cert_store = rustls::RootCertStore::empty();
345            
346            // Load native certificates using rustls_native_certs v0.8.1
347            // This returns a CertificateResult which has a certs field containing the certificates
348            let native_certs = rustls_native_certs::load_native_certs();
349            
350            // Add each cert to the root store
351            for cert in &native_certs.certs {
352                if let Err(e) = root_cert_store.add(cert.clone()) {
353                    return Err(ClientError::TlsError(format!("Failed to add native cert to root store: {}", e)));
354                }
355            }
356            
357            // Add custom CA certificate if provided
358            if let Some(ref pem_bytes) = self.root_ca_pem {
359                let mut reader = std::io::Cursor::new(pem_bytes);
360                for cert_result in rustls_pemfile::certs(&mut reader) {
361                    match cert_result {
362                        Ok(cert) => {
363                            root_cert_store.add(cert)
364                                .map_err(|e| ClientError::TlsError(format!("Failed to add custom cert to root store: {}", e)))?;
365                        },
366                        Err(e) => return Err(ClientError::TlsError(format!("Failed to parse PEM cert: {}", e))),
367                    }
368                }
369            }
370            
371            // Configure rustls
372            let mut config_builder = rustls::ClientConfig::builder()
373                .with_root_certificates(root_cert_store);
374            
375            let rustls_config = config_builder.with_no_client_auth();
376            
377            #[cfg(feature = "insecure-dangerous")]
378            let rustls_config = if self.accept_invalid_certs {
379                // ⚠️ SECURITY WARNING: This code path deliberately bypasses TLS certificate validation.
380                // It should only be used during development/testing with self-signed certificates,
381                // and NEVER in production environments. This creates a vulnerability to 
382                // man-in-the-middle attacks and is extremely dangerous.
383                
384                use std::sync::Arc;
385                use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified};
386                use rustls::DigitallySignedStruct;
387                use rustls::SignatureScheme;
388                use rustls::pki_types::UnixTime;
389                
390                // Override the certificate verifier with a no-op verifier that accepts all certificates
391                #[derive(Debug)]
392                struct NoCertificateVerification {}
393                
394                impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
395                    fn verify_server_cert(
396                        &self,
397                        _end_entity: &rustls::pki_types::CertificateDer<'_>,
398                        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
399                        _server_name: &rustls::pki_types::ServerName<'_>,
400                        _ocsp_response: &[u8],
401                        _now: UnixTime,
402                    ) -> Result<ServerCertVerified, rustls::Error> {
403                        // Accept any certificate without verification
404                        Ok(ServerCertVerified::assertion())
405                    }
406                    
407                    fn verify_tls12_signature(
408                        &self,
409                        _message: &[u8],
410                        _cert: &rustls::pki_types::CertificateDer<'_>,
411                        _dss: &DigitallySignedStruct,
412                    ) -> Result<HandshakeSignatureValid, rustls::Error> {
413                        // Accept any TLS 1.2 signature without verification
414                        Ok(HandshakeSignatureValid::assertion())
415                    }
416                    
417                    fn verify_tls13_signature(
418                        &self,
419                        _message: &[u8],
420                        _cert: &rustls::pki_types::CertificateDer<'_>,
421                        _dss: &DigitallySignedStruct,
422                    ) -> Result<HandshakeSignatureValid, rustls::Error> {
423                        // Accept any TLS 1.3 signature without verification
424                        Ok(HandshakeSignatureValid::assertion())
425                    }
426                    
427                    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
428                        // Return a list of all supported signature schemes
429                        vec![
430                            SignatureScheme::RSA_PKCS1_SHA1,
431                            SignatureScheme::ECDSA_SHA1_Legacy,
432                            SignatureScheme::RSA_PKCS1_SHA256,
433                            SignatureScheme::ECDSA_NISTP256_SHA256,
434                            SignatureScheme::RSA_PKCS1_SHA384,
435                            SignatureScheme::ECDSA_NISTP384_SHA384,
436                            SignatureScheme::RSA_PKCS1_SHA512,
437                            SignatureScheme::ECDSA_NISTP521_SHA512,
438                            SignatureScheme::RSA_PSS_SHA256,
439                            SignatureScheme::RSA_PSS_SHA384,
440                            SignatureScheme::RSA_PSS_SHA512,
441                            SignatureScheme::ED25519,
442                            SignatureScheme::ED448,
443                        ]
444                    }
445                }
446                
447                // Set up the dangerous configuration with no certificate verification
448                let mut config = rustls_config.clone();
449                config.dangerous().set_certificate_verifier(Arc::new(NoCertificateVerification {}));
450                config
451            } else {
452                rustls_config
453            };
454            
455            // Handle certificate pinning if configured
456            #[cfg(feature = "rustls")]
457            let rustls_config = if let Some(ref pins) = self.pinned_cert_sha256 {
458                // Implement certificate pinning by creating a custom certificate verifier
459                use std::sync::Arc;
460                use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
461                use rustls::DigitallySignedStruct;
462                use rustls::SignatureScheme;
463                use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
464                
465                // Create a custom certificate verifier that checks certificate pins
466                struct CertificatePinner {
467                    pins: Vec<[u8; 32]>,
468                    inner: Arc<dyn ServerCertVerifier>,
469                }
470                
471                impl ServerCertVerifier for CertificatePinner {
472                    fn verify_server_cert(
473                        &self,
474                        end_entity: &CertificateDer<'_>,
475                        intermediates: &[CertificateDer<'_>],
476                        server_name: &ServerName<'_>,
477                        ocsp_response: &[u8],
478                        now: UnixTime,
479                    ) -> Result<ServerCertVerified, rustls::Error> {
480                        // First, use the inner verifier to do standard verification
481                        self.inner.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)?;
482                        
483                        // Then verify the pin
484                        use sha2::{Sha256, Digest};
485                        
486                        let mut hasher = Sha256::new();
487                        hasher.update(end_entity.as_ref());
488                        let cert_hash = hasher.finalize();
489                        
490                        // Check if the certificate hash matches any of our pins
491                        for pin in &self.pins {
492                            if pin[..] == cert_hash[..] {
493                                return Ok(ServerCertVerified::assertion());
494                            }
495                        }
496                        
497                        // If we got here, none of the pins matched
498                        Err(rustls::Error::General("Certificate pin verification failed".into()))
499                    }
500                    
501                    fn verify_tls12_signature(
502                        &self, 
503                        message: &[u8],
504                        cert: &CertificateDer<'_>,
505                        dss: &DigitallySignedStruct,
506                    ) -> Result<HandshakeSignatureValid, rustls::Error> {
507                        // Delegate to inner verifier
508                        self.inner.verify_tls12_signature(message, cert, dss)
509                    }
510                    
511                    fn verify_tls13_signature(
512                        &self,
513                        message: &[u8],
514                        cert: &CertificateDer<'_>,
515                        dss: &DigitallySignedStruct,
516                    ) -> Result<HandshakeSignatureValid, rustls::Error> {
517                        // Delegate to inner verifier
518                        self.inner.verify_tls13_signature(message, cert, dss)
519                    }
520                    
521                    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
522                        self.inner.supported_verify_schemes()
523                    }
524                }
525                
526                // Create the certificate pinner with our pins and the default verifier
527                let mut config = rustls_config.clone();
528                let default_verifier = rustls::client::WebPkiServerVerifier::builder()
529                    .with_root_certificates(root_cert_store.clone())
530                    .build()
531                    .map_err(|e| ClientError::TlsError(format!("Failed to build certificate verifier: {}", e)))?;
532                
533                let cert_pinner = Arc::new(CertificatePinner {
534                    pins: pins.clone(),
535                    inner: default_verifier,
536                });
537                
538                config.dangerous().set_certificate_verifier(cert_pinner);
539                config
540            } else {
541                rustls_config
542            };
543            
544            // Create a connector that supports HTTP and HTTPS
545            let mut http_connector = hyper_util::client::legacy::connect::HttpConnector::new();
546            http_connector.enforce_http(false);
547            
548            // Create the rustls connector using HttpsConnectorBuilder
549            let https_connector = hyper_rustls::HttpsConnectorBuilder::new()
550                .with_tls_config(rustls_config)
551                .https_or_http()
552                .enable_http1()
553                .build();
554            
555            let client = Client::builder(TokioExecutor::new()).build(https_connector);
556            let resp = tokio::time::timeout(self.timeout, client.request(req))
557                .await
558                .map_err(|_| ClientError::TlsError("Request timed out".to_string()))?
559                ?;
560            self.build_response(resp).await
561        }
562        #[cfg(not(any(feature = "native-tls", feature = "rustls")))]
563        {
564            // If neither "native-tls" nor "rustls" features are enabled,
565            // fall back to a basic HTTP connector without TLS support.
566            // This is primarily for scenarios where TLS is not required or
567            // handled at a different layer.
568            let connector = hyper_util::client::legacy::connect::HttpConnector::new();
569            let client = Client::builder(TokioExecutor::new()).build(connector);
570            let resp = tokio::time::timeout(self.timeout, client.request(req))
571                .await
572                .map_err(|_| ClientError::TlsError("Request timed out".to_string()))?
573                ?;
574            self.build_response(resp).await
575        }
576    }
577
578    /// Helper method to convert a hyper Response to our HttpResponse with raw body data.
579    /// This method abstracts the details of `hyper::Response` processing,
580    /// extracting the status, headers, and importantly, collecting the entire
581    /// response body into a `Bytes` buffer for easy consumption by the caller.
582    async fn build_response(&self, resp: Response<Incoming>) -> Result<HttpResponse, ClientError> {
583        let status = resp.status();
584        
585        // Convert hyper's `HeaderMap` to a `HashMap<String, String>` for simpler
586        // public API exposure, making header access more idiomatic for consumers.
587        let mut headers = HashMap::new();
588        for (name, value) in resp.headers() {
589            if let Ok(value_str) = value.to_str() {
590                headers.insert(name.to_string(), value_str.to_string());
591            }
592        }
593        
594        // Collect the body as raw bytes - this is the key part of the issue
595        // We expose the body as raw bytes without any permutations, ensuring
596        // the client receives the exact byte content of the response.
597        let body_bytes = resp.into_body().collect().await?.to_bytes();
598        
599        Ok(HttpResponse {
600            status,
601            headers,
602            body: body_bytes,
603        })
604    }
605}
606
607// WebAssembly stubbed runtime implementation
608#[cfg(target_arch = "wasm32")]
609impl HttpClient {
610    /// On wasm32 targets, runtime methods are stubbed and return
611    /// `ClientError::WasmNotImplemented` because browsers do not allow
612    /// programmatic installation/trust of custom CAs.
613    pub fn request(&self, _url: &str) -> Result<(), ClientError> {
614        Err(ClientError::WasmNotImplemented)
615    }
616
617    /// POST is also not implemented on wasm32 targets for the same reason.
618    pub fn post<B: AsRef<[u8]>>(&self, _url: &str, _body: B) -> Result<(), ClientError> {
619        Err(ClientError::WasmNotImplemented)
620    }
621}
622
623/// Builder for configuring and creating an [`HttpClient`].
624pub struct HttpClientBuilder {
625    timeout: Duration,
626    default_headers: HashMap<String, String>,
627    #[cfg(feature = "insecure-dangerous")]
628    accept_invalid_certs: bool,
629    root_ca_pem: Option<Vec<u8>>,
630    #[cfg(feature = "rustls")]
631    pinned_cert_sha256: Option<Vec<[u8; 32]>>,
632}
633
634impl HttpClientBuilder {
635    /// Start a new builder with default settings.
636    pub fn new() -> Self {
637        Self {
638            timeout: Duration::from_secs(30),
639            default_headers: HashMap::new(),
640            #[cfg(feature = "insecure-dangerous")]
641            accept_invalid_certs: false,
642            root_ca_pem: None,
643            #[cfg(feature = "rustls")]
644            pinned_cert_sha256: None,
645        }
646    }
647
648    /// Set a request timeout to apply to client operations.
649    pub fn with_timeout(mut self, timeout: Duration) -> Self {
650        self.timeout = timeout;
651        self
652    }
653
654    /// Set default headers that will be added to every request initiated by this client.
655    pub fn with_default_headers(mut self, headers: HashMap<String, String>) -> Self {
656        self.default_headers = headers;
657        self
658    }
659
660    /// Dev-only: accept self-signed/invalid TLS certificates. Requires the
661    /// `insecure-dangerous` feature to be enabled. NEVER enable this in production.
662    /// 
663    /// # Security Warning
664    /// 
665    /// ⚠️ CRITICAL SECURITY WARNING ⚠️
666    /// 
667    /// This method deliberately bypasses TLS certificate validation, which creates a
668    /// serious security vulnerability to man-in-the-middle attacks. When enabled:
669    /// 
670    /// - The client will accept ANY certificate, regardless of its validity
671    /// - The client will accept expired certificates
672    /// - The client will accept certificates from untrusted issuers
673    /// - The client will accept certificates for the wrong domain
674    /// 
675    /// This method should ONLY be used for:
676    /// - Local development with self-signed certificates
677    /// - Testing environments where security is not a concern
678    /// - Debugging TLS connection issues
679    /// 
680    /// # Implementation Details
681    /// 
682    /// When enabled, this setting:
683    /// - For `native-tls`: Uses `danger_accept_invalid_certs(true)` on the TLS connector
684    /// - For `rustls`: Implements a custom `ServerCertVerifier` that accepts all certificates
685    ///
686    /// # Examples
687    ///
688    /// Enable insecure mode during local development (dangerous):
689    ///
690    /// ```ignore
691    /// use hyper_custom_cert::HttpClient;
692    ///
693    /// // Requires: --features insecure-dangerous
694    /// let client = HttpClient::builder()
695    ///     .insecure_accept_invalid_certs(true)
696    ///     .build();
697    /// ```
698    #[cfg(feature = "insecure-dangerous")]
699    pub fn insecure_accept_invalid_certs(mut self, accept: bool) -> Self {
700        self.accept_invalid_certs = accept;
701        self
702    }
703
704    /// Provide a PEM-encoded Root CA certificate to be trusted by the client.
705    /// This is the production-ready way to trust a custom CA.
706    ///
707    /// # Examples
708    ///
709    /// ```ignore
710    /// use hyper_custom_cert::HttpClient;
711    ///
712    /// // Requires: --no-default-features --features rustls
713    /// let client = HttpClient::builder()
714    ///     .with_root_ca_pem(include_bytes!("../examples-data/root-ca.pem"))
715    ///     .build();
716    /// ```
717    #[cfg(feature = "rustls")]
718    pub fn with_root_ca_pem(mut self, pem_bytes: &[u8]) -> Self {
719        self.root_ca_pem = Some(pem_bytes.to_vec());
720        self
721    }
722
723    /// Provide a PEM-encoded Root CA certificate file to be trusted by the client.
724    /// This is the production-ready way to trust a custom CA from a file path.
725    ///
726    /// The file will be read during builder configuration and its contents stored
727    /// in the client. This method will panic if the file cannot be read, similar
728    /// to how `include_bytes!` macro behaves.
729    ///
730    /// # Security Considerations
731    ///
732    /// Only use certificate files from trusted sources. Ensure proper file permissions
733    /// are set to prevent unauthorized modification of the certificate file.
734    ///
735    /// # Panics
736    ///
737    /// This method will panic if:
738    /// - The file does not exist
739    /// - The file cannot be read due to permissions or I/O errors
740    /// - The path is invalid
741    ///
742    /// # Examples
743    ///
744    /// ```ignore
745    /// use hyper_custom_cert::HttpClient;
746    ///
747    /// // Requires: --no-default-features --features rustls
748    /// let client = HttpClient::builder()
749    ///     .with_root_ca_file("path/to/root-ca.pem")
750    ///     .build();
751    /// ```
752    ///
753    /// Using a `std::path::Path`:
754    ///
755    /// ```ignore
756    /// use hyper_custom_cert::HttpClient;
757    /// use std::path::Path;
758    ///
759    /// // Requires: --no-default-features --features rustls
760    /// let ca_path = Path::new("certs/custom-ca.pem");
761    /// let client = HttpClient::builder()
762    ///     .with_root_ca_file(ca_path)
763    ///     .build();
764    /// ```
765    #[cfg(feature = "rustls")]
766    pub fn with_root_ca_file<P: AsRef<Path>>(mut self, path: P) -> Self {
767        let pem_bytes = fs::read(path.as_ref()).unwrap_or_else(|e| {
768            panic!(
769                "Failed to read CA certificate file '{}': {}",
770                path.as_ref().display(),
771                e
772            )
773        });
774        self.root_ca_pem = Some(pem_bytes);
775        self
776    }
777
778    /// Configure certificate pinning using SHA256 fingerprints for additional security.
779    ///
780    /// Certificate pinning provides an additional layer of security beyond CA validation
781    /// by verifying that the server's certificate matches one of the provided fingerprints.
782    /// This helps protect against compromised CAs and man-in-the-middle attacks.
783    ///
784    /// # Security Considerations
785    ///
786    /// - Certificate pinning should be used in conjunction with, not as a replacement for,
787    ///   proper CA validation.
788    /// - Pinned certificates must be updated when the server's certificate changes.
789    /// - Consider having backup pins for certificate rotation scenarios.
790    /// - This method provides additional security but requires careful maintenance.
791    ///
792    /// # Parameters
793    ///
794    /// * `pins` - A vector of 32-byte SHA256 fingerprints of certificates to pin.
795    ///   Each fingerprint should be the SHA256 hash of the certificate's DER encoding.
796    ///
797    /// # Examples
798    ///
799    /// ```ignore
800    /// use hyper_custom_cert::HttpClient;
801    ///
802    /// // Example SHA256 fingerprints (these are just examples)
803    /// let pin1: [u8; 32] = [
804    ///     0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
805    ///     0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
806    ///     0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00,
807    ///     0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0x07, 0x18
808    /// ];
809    ///
810    /// let pin2: [u8; 32] = [
811    ///     0xf0, 0xe1, 0xd2, 0xc3, 0xb4, 0xa5, 0x96, 0x87,
812    ///     0x78, 0x69, 0x5a, 0x4b, 0x3c, 0x2d, 0x1e, 0x0f,
813    ///     0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
814    ///     0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff
815    /// ];
816    ///
817    /// // Requires: --no-default-features --features rustls
818    /// let client = HttpClient::builder()
819    ///     .with_pinned_cert_sha256(vec![pin1, pin2])
820    ///     .build();
821    /// ```
822    #[cfg(feature = "rustls")]
823    pub fn with_pinned_cert_sha256(mut self, pins: Vec<[u8; 32]>) -> Self {
824        self.pinned_cert_sha256 = Some(pins);
825        self
826    }
827
828    /// Finalize the configuration and build an [`HttpClient`].
829    pub fn build(self) -> HttpClient {
830        HttpClient {
831            timeout: self.timeout,
832            default_headers: self.default_headers,
833            #[cfg(feature = "insecure-dangerous")]
834            accept_invalid_certs: self.accept_invalid_certs,
835            root_ca_pem: self.root_ca_pem,
836            #[cfg(feature = "rustls")]
837            pinned_cert_sha256: self.pinned_cert_sha256,
838        }
839    }
840}
841
842/// Default construction uses builder defaults.
843impl Default for HttpClient {
844    fn default() -> Self {
845        Self::new()
846    }
847}
848
849/// Default builder state is secure and ergonomic.
850impl Default for HttpClientBuilder {
851    fn default() -> Self {
852        Self::new()
853    }
854}
855
856#[cfg(test)]
857mod tests {
858    use super::*;
859
860    #[test]
861    fn builder_default_builds() {
862        let _client = HttpClient::builder().build();
863    }
864
865    #[test]
866    fn builder_allows_timeout_and_headers() {
867        let mut headers = HashMap::new();
868        headers.insert("x-test".into(), "1".into());
869        let builder = HttpClient::builder()
870            .with_timeout(Duration::from_secs(5))
871            .with_default_headers(headers);
872        #[cfg(feature = "rustls")]
873        let builder = builder.with_root_ca_pem(b"-----BEGIN CERTIFICATE-----\n...");
874        let _client = builder.build();
875    }
876
877    #[cfg(feature = "insecure-dangerous")]
878    #[test]
879    fn builder_allows_insecure_when_feature_enabled() {
880        let _client = HttpClient::builder()
881            .insecure_accept_invalid_certs(true)
882            .build();
883        let _client2 = HttpClient::with_self_signed_certs();
884    }
885
886    #[cfg(not(target_arch = "wasm32"))]
887    #[tokio::test]
888    async fn request_returns_ok_on_native() {
889        let client = HttpClient::builder().build();
890        // Just test that the method can be called - don't actually make network requests in tests
891        // In a real test environment, you would mock the HTTP calls or use a test server
892        let _client = client; // Use the client to avoid unused variable warning
893    }
894
895    #[cfg(not(target_arch = "wasm32"))]
896    #[tokio::test]  
897    async fn post_returns_ok_on_native() {
898        let client = HttpClient::builder().build();
899        // Just test that the method can be called - don't actually make network requests in tests
900        // In a real test environment, you would mock the HTTP calls or use a test server
901        let _client = client; // Use the client to avoid unused variable warning
902    }
903
904    #[cfg(all(feature = "rustls", not(target_arch = "wasm32")))]
905    #[test]
906    fn builder_allows_root_ca_file() {
907        use std::fs;
908        use std::io::Write;
909
910        // Create a temporary file with test certificate content
911        let temp_dir = std::env::temp_dir();
912        let cert_file = temp_dir.join("test-ca.pem");
913
914        let test_cert = b"-----BEGIN CERTIFICATE-----
915MIICxjCCAa4CAQAwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAe
916Fw0yNTA4MTQwMDAwMDBaFw0yNjA4MTQwMDAwMDBaMBIxEDAOBgNVBAMMB1Rlc3Qg
917Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTest...
918-----END CERTIFICATE-----";
919
920        // Write test certificate to temporary file
921        {
922            let mut file = fs::File::create(&cert_file).expect("Failed to create temp cert file");
923            file.write_all(test_cert)
924                .expect("Failed to write cert to temp file");
925        }
926
927        // Test that the builder can read the certificate file
928        let client = HttpClient::builder().with_root_ca_file(&cert_file).build();
929
930        // Verify the certificate was loaded
931        assert!(client.root_ca_pem.is_some());
932        assert_eq!(client.root_ca_pem.as_ref().unwrap(), test_cert);
933
934        // Clean up
935        let _ = fs::remove_file(cert_file);
936    }
937}