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