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}