diff --git a/crates/example/src/main.rs b/crates/example/src/main.rs index 66e7d19..c14963b 100644 --- a/crates/example/src/main.rs +++ b/crates/example/src/main.rs @@ -1,12 +1,12 @@ use axum::{ + Router, extract::{Path, Query}, response::Json, - routing::{get, post, put, delete}, - Router, + routing::{delete, get, post, put}, }; use hyper_custom_cert::HttpClient; use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; +use serde_json::{Value, json}; use std::collections::HashMap; use std::time::Duration; @@ -38,39 +38,35 @@ async fn main() { let app = Router::new() // Root endpoint with API overview .route("/", get(api_overview)) - // Basic HTTP client tests .route("/test/client/default", get(test_default_client)) .route("/test/client/builder", get(test_builder_client)) .route("/test/client/timeout", get(test_timeout_client)) .route("/test/client/headers", get(test_headers_client)) .route("/test/client/combined", get(test_combined_config)) - // Feature-specific tests .route("/test/features/native-tls", get(test_native_tls_feature)) .route("/test/features/rustls", get(test_rustls_feature)) .route("/test/features/insecure", get(test_insecure_feature)) - // HTTP method tests .route("/test/methods/get", get(test_get_method)) .route("/test/methods/post", post(test_post_method)) .route("/test/methods/put", put(test_put_method)) .route("/test/methods/delete", delete(test_delete_method)) - // Certificate and TLS tests .route("/test/tls/custom-ca", get(test_custom_ca)) .route("/test/tls/cert-pinning", get(test_cert_pinning)) .route("/test/tls/self-signed", get(test_self_signed)) - // Configuration tests .route("/test/config/timeout/{seconds}", get(test_custom_timeout)) - .route("/test/config/headers/{header_count}", get(test_custom_headers)) - + .route( + "/test/config/headers/{header_count}", + get(test_custom_headers), + ) // Error simulation tests .route("/test/errors/timeout", get(test_timeout_error)) .route("/test/errors/invalid-url", get(test_invalid_url)) .route("/test/errors/connection", get(test_connection_error)) - // Health and status endpoints .route("/health", get(health_check)) .route("/status", get(status_check)); @@ -80,7 +76,7 @@ async fn main() { println!("📍 Listening on http://{}", SERVER_ADDRESS); println!("📖 Visit http://{} for API documentation", SERVER_ADDRESS); println!("🧪 Ready for integration testing!"); - + axum::serve(listener, app).await.unwrap(); } @@ -130,7 +126,7 @@ async fn api_overview() -> Json { }, "features_available": [ "native-tls", - "rustls", + "rustls", "insecure-dangerous" ] })) @@ -144,7 +140,7 @@ async fn api_overview() -> Json { async fn test_default_client() -> Json { let client = HttpClient::new(); let result = client.request("https://httpbin.org/get").await; - + Json(TestResponse { endpoint: "/test/client/default".to_string(), status: "success".to_string(), @@ -161,7 +157,7 @@ async fn test_default_client() -> Json { async fn test_builder_client() -> Json { let client = HttpClient::builder().build(); let result = client.request("https://httpbin.org/get").await; - + Json(TestResponse { endpoint: "/test/client/builder".to_string(), status: "success".to_string(), @@ -181,11 +177,14 @@ async fn test_timeout_client(Query(params): Query) -> Json None, @@ -197,15 +196,16 @@ async fn test_timeout_client(Query(params): Query) -> Json Json { let mut headers = HashMap::new(); - headers.insert("User-Agent".to_string(), "hyper-custom-cert-test/1.0".to_string()); + headers.insert( + "User-Agent".to_string(), + "hyper-custom-cert-test/1.0".to_string(), + ); headers.insert("X-Test-Header".to_string(), "test-value".to_string()); headers.insert("Accept".to_string(), "application/json".to_string()); - - let client = HttpClient::builder() - .with_default_headers(headers) - .build(); + + let client = HttpClient::builder().with_default_headers(headers).build(); let result = client.request("https://httpbin.org/get").await; - + Json(TestResponse { endpoint: "/test/client/headers".to_string(), status: "success".to_string(), @@ -221,19 +221,23 @@ async fn test_headers_client() -> Json { /// Test combined configuration options async fn test_combined_config() -> Json { let mut headers = HashMap::new(); - headers.insert("User-Agent".to_string(), "hyper-custom-cert-combined/1.0".to_string()); + headers.insert( + "User-Agent".to_string(), + "hyper-custom-cert-combined/1.0".to_string(), + ); headers.insert("X-Combined-Test".to_string(), "true".to_string()); - + let client = HttpClient::builder() .with_timeout(Duration::from_secs(30)) .with_default_headers(headers) .build(); let result = client.request("https://httpbin.org/get").await; - + Json(TestResponse { endpoint: "/test/client/combined".to_string(), status: "success".to_string(), - message: "HttpClient with combined configuration (timeout + headers) works correctly".to_string(), + message: "HttpClient with combined configuration (timeout + headers) works correctly" + .to_string(), features_tested: vec!["timeout-config".to_string(), "custom-headers".to_string()], error: match result { Ok(_) => None, @@ -254,7 +258,7 @@ async fn test_native_tls_feature() -> Json { .with_timeout(Duration::from_secs(10)) .build(); let result = client.request("https://httpbin.org/get").await; - + Json(TestResponse { endpoint: "/test/features/native-tls".to_string(), status: "success".to_string(), @@ -284,13 +288,13 @@ async fn test_rustls_feature() -> Json { { // Test with sample root CA PEM (this is just a demo cert) let ca_pem: &[u8] = b"-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow\nPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\nEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\nrz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\nOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\nxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\naeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\nSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\nikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\nAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\nR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\nJDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\nOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n-----END CERTIFICATE-----\n"; - + let client = HttpClient::builder() .with_timeout(Duration::from_secs(10)) .with_root_ca_pem(ca_pem) .build(); let result = client.request("https://httpbin.org/get").await; - + Json(TestResponse { endpoint: "/test/features/rustls".to_string(), status: "success".to_string(), @@ -321,17 +325,18 @@ async fn test_insecure_feature() -> Json { // Test shortcut method let client = HttpClient::with_self_signed_certs(); let result = client.request("https://self-signed.badssl.com/").await; - + // Test builder method let client2 = HttpClient::builder() .insecure_accept_invalid_certs(true) .build(); let result2 = client2.request("https://expired.badssl.com/").await; - + Json(TestResponse { endpoint: "/test/features/insecure".to_string(), status: "success".to_string(), - message: "insecure-dangerous feature is working (DO NOT USE IN PRODUCTION!)".to_string(), + message: "insecure-dangerous feature is working (DO NOT USE IN PRODUCTION!)" + .to_string(), features_tested: vec!["insecure-dangerous".to_string()], error: match (result, result2) { (Ok(_), Ok(_)) => None, @@ -345,7 +350,8 @@ async fn test_insecure_feature() -> Json { Json(TestResponse { endpoint: "/test/features/insecure".to_string(), status: "skipped".to_string(), - message: "insecure-dangerous feature is not enabled (this is good for security!)".to_string(), + message: "insecure-dangerous feature is not enabled (this is good for security!)" + .to_string(), features_tested: vec![], error: Some("Feature not enabled".to_string()), }) @@ -360,7 +366,7 @@ async fn test_insecure_feature() -> Json { async fn test_get_method() -> Json { let client = HttpClient::new(); let result = client.request("https://httpbin.org/get").await; - + Json(TestResponse { endpoint: "/test/methods/get".to_string(), status: "success".to_string(), @@ -378,11 +384,14 @@ async fn test_post_method(Json(payload): Json) -> Json { let client = HttpClient::new(); let body = serde_json::to_vec(&payload).unwrap_or_default(); let result = client.post("https://httpbin.org/post", &body).await; - + Json(TestResponse { endpoint: "/test/methods/post".to_string(), status: "success".to_string(), - message: format!("HTTP POST method test completed with data: {}", payload.data), + message: format!( + "HTTP POST method test completed with data: {}", + payload.data + ), features_tested: vec!["post-request".to_string()], error: match result { Ok(_) => None, @@ -396,11 +405,14 @@ async fn test_put_method(Json(payload): Json) -> Json { let client = HttpClient::new(); let body = serde_json::to_vec(&payload).unwrap_or_default(); let result = client.post("https://httpbin.org/put", &body).await; - + Json(TestResponse { endpoint: "/test/methods/put".to_string(), status: "success".to_string(), - message: format!("HTTP PUT method test completed (simulated via POST) with data: {}", payload.data), + message: format!( + "HTTP PUT method test completed (simulated via POST) with data: {}", + payload.data + ), features_tested: vec!["put-request-simulation".to_string()], error: match result { Ok(_) => None, @@ -413,7 +425,7 @@ async fn test_put_method(Json(payload): Json) -> Json { async fn test_delete_method() -> Json { let client = HttpClient::new(); let result = client.request("https://httpbin.org/delete").await; - + Json(TestResponse { endpoint: "/test/methods/delete".to_string(), status: "success".to_string(), @@ -435,7 +447,7 @@ async fn test_custom_ca() -> Json { #[cfg(feature = "rustls")] { let ca_pem: &[u8] = b"-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow\nPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\nEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\nrz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\nOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\nxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\naeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\nSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\nikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\nAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\nR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\nJDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\nOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n-----END CERTIFICATE-----\n"; - + let client = HttpClient::builder() .with_timeout(Duration::from_secs(10)) .with_root_ca_pem(ca_pem) @@ -443,7 +455,7 @@ async fn test_custom_ca() -> Json { let result = client.request("https://httpbin.org/get"); let awaited = result.await; - + Json(TestResponse { endpoint: "/test/tls/custom-ca".to_string(), status: "success".to_string(), @@ -472,13 +484,12 @@ async fn test_cert_pinning() -> Json { #[cfg(feature = "rustls")] { // Example SHA256 fingerprints (these are demo values) - let pins = vec![ - [0x1f, 0x2f, 0x3f, 0x4f, 0x5f, 0x6f, 0x7f, 0x8f, - 0x9f, 0xaf, 0xbf, 0xcf, 0xdf, 0xef, 0xff, 0x0f, - 0x1f, 0x2f, 0x3f, 0x4f, 0x5f, 0x6f, 0x7f, 0x8f, - 0x9f, 0xaf, 0xbf, 0xcf, 0xdf, 0xef, 0xff, 0x0f], - ]; - + let pins = vec![[ + 0x1f, 0x2f, 0x3f, 0x4f, 0x5f, 0x6f, 0x7f, 0x8f, 0x9f, 0xaf, 0xbf, 0xcf, 0xdf, 0xef, + 0xff, 0x0f, 0x1f, 0x2f, 0x3f, 0x4f, 0x5f, 0x6f, 0x7f, 0x8f, 0x9f, 0xaf, 0xbf, 0xcf, + 0xdf, 0xef, 0xff, 0x0f, + ]]; + let client = HttpClient::builder() .with_timeout(Duration::from_secs(10)) .with_pinned_cert_sha256(pins) @@ -486,7 +497,7 @@ async fn test_cert_pinning() -> Json { let result = client.request("https://httpbin.org/get"); let awaited = result.await; - + Json(TestResponse { endpoint: "/test/tls/cert-pinning".to_string(), status: "success".to_string(), @@ -518,7 +529,7 @@ async fn test_self_signed() -> Json { let result = client.request("https://self-signed.badssl.com/"); let awaited = result.await; - + Json(TestResponse { endpoint: "/test/tls/self-signed".to_string(), status: "success".to_string(), @@ -535,7 +546,8 @@ async fn test_self_signed() -> Json { Json(TestResponse { endpoint: "/test/tls/self-signed".to_string(), status: "skipped".to_string(), - message: "Self-signed test requires insecure-dangerous feature (good for security!)".to_string(), + message: "Self-signed test requires insecure-dangerous feature (good for security!)" + .to_string(), features_tested: vec![], error: Some("insecure-dangerous feature not enabled".to_string()), }) @@ -549,11 +561,9 @@ async fn test_self_signed() -> Json { /// Test custom timeout configuration async fn test_custom_timeout(Path(seconds): Path) -> Json { let timeout_duration = Duration::from_secs(seconds); - let client = HttpClient::builder() - .with_timeout(timeout_duration) - .build(); + let client = HttpClient::builder().with_timeout(timeout_duration).build(); let result = client.request("https://httpbin.org/delay/1"); - + Json(TestResponse { endpoint: format!("/test/config/timeout/{}", seconds), status: "success".to_string(), @@ -569,28 +579,31 @@ async fn test_custom_timeout(Path(seconds): Path) -> Json { /// Test custom headers configuration async fn test_custom_headers(Path(header_count): Path) -> Json { let mut headers = HashMap::new(); - + for i in 0..header_count { - headers.insert( - format!("X-Test-Header-{}", i), - format!("test-value-{}", i), - ); + headers.insert(format!("X-Test-Header-{}", i), format!("test-value-{}", i)); } - + // Add some standard headers - headers.insert("User-Agent".to_string(), "hyper-custom-cert-headers-test/1.0".to_string()); + headers.insert( + "User-Agent".to_string(), + "hyper-custom-cert-headers-test/1.0".to_string(), + ); headers.insert("Accept".to_string(), "application/json".to_string()); - + let client = HttpClient::builder() .with_timeout(Duration::from_secs(10)) .with_default_headers(headers) .build(); let result = client.request("https://httpbin.org/headers"); - + Json(TestResponse { endpoint: format!("/test/config/headers/{}", header_count), status: "success".to_string(), - message: format!("Custom headers test with {} headers completed", header_count + 2), + message: format!( + "Custom headers test with {} headers completed", + header_count + 2 + ), features_tested: vec!["custom-headers".to_string()], error: match result.await { Ok(_) => None, @@ -614,7 +627,12 @@ async fn test_timeout_error() -> Json { let awaited = result.await; Json(TestResponse { endpoint: "/test/errors/timeout".to_string(), - status: if awaited.is_err() { "success" } else { "unexpected" }.to_string(), + status: if awaited.is_err() { + "success" + } else { + "unexpected" + } + .to_string(), message: "Timeout error simulation test completed".to_string(), features_tested: vec!["timeout-error-handling".to_string()], error: match awaited { @@ -633,7 +651,12 @@ async fn test_invalid_url() -> Json { Json(TestResponse { endpoint: "/test/errors/invalid-url".to_string(), - status: if awaited.is_err() { "success" } else { "unexpected" }.to_string(), + status: if awaited.is_err() { + "success" + } else { + "unexpected" + } + .to_string(), message: "Invalid URL error simulation test completed".to_string(), features_tested: vec!["url-validation".to_string()], error: match awaited { @@ -654,7 +677,12 @@ async fn test_connection_error() -> Json { Json(TestResponse { endpoint: "/test/errors/connection".to_string(), - status: if awaited.is_err() { "success" } else { "unexpected" }.to_string(), + status: if awaited.is_err() { + "success" + } else { + "unexpected" + } + .to_string(), message: "Connection error simulation test completed".to_string(), features_tested: vec!["connection-error-handling".to_string()], error: match awaited { @@ -675,7 +703,7 @@ async fn health_check() -> Json { .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); - + Json(json!({ "status": "healthy", "timestamp": timestamp, @@ -691,13 +719,13 @@ async fn status_check() -> Json { .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); - + // Test basic client creation to verify library is working let client_test = match HttpClient::new().request("https://httpbin.org/get").await { Ok(_) => "operational", - Err(_) => "degraded" + Err(_) => "degraded", }; - + Json(json!({ "service": "hyper-custom-cert-test-harness", "version": "1.0.0", @@ -711,7 +739,7 @@ async fn status_check() -> Json { "endpoints_available": 18, "test_categories": [ "basic_client_tests", - "feature_specific_tests", + "feature_specific_tests", "http_method_tests", "tls_certificate_tests", "configuration_tests", @@ -719,4 +747,4 @@ async fn status_check() -> Json { "utility_endpoints" ] })) -} \ No newline at end of file +} diff --git a/crates/hyper-custom-cert/src/lib.rs b/crates/hyper-custom-cert/src/lib.rs index 3bfeb24..80c712b 100644 --- a/crates/hyper-custom-cert/src/lib.rs +++ b/crates/hyper-custom-cert/src/lib.rs @@ -36,10 +36,10 @@ use std::path::Path; use std::time::Duration; use bytes::Bytes; -use hyper::{body::Incoming, Request, Response, StatusCode, Uri, Method}; +use http_body_util::BodyExt; +use hyper::{Method, Request, Response, StatusCode, Uri, body::Incoming}; use hyper_util::client::legacy::Client; use hyper_util::rt::TokioExecutor; -use http_body_util::BodyExt; /// Options for controlling HTTP requests. /// @@ -81,13 +81,13 @@ impl RequestOptions { pub fn new() -> Self { RequestOptions::default() } - + /// Add custom headers to this request. pub fn with_headers(mut self, headers: HashMap) -> Self { self.headers = Some(headers); self } - + /// Override the client's default timeout for this request. pub fn with_timeout(mut self, timeout: Duration) -> Self { self.timeout = Some(timeout); @@ -239,32 +239,32 @@ impl HttpClient { /// Convenience constructor that enables acceptance of self-signed/invalid /// certificates. This is gated behind the `insecure-dangerous` feature and intended /// strictly for development and testing. NEVER enable in production. - /// + /// /// # Security Warning - /// + /// /// ⚠️ CRITICAL SECURITY WARNING ⚠️ - /// + /// /// This method deliberately bypasses TLS certificate validation, creating a /// serious security vulnerability to man-in-the-middle attacks. When used: - /// + /// /// - ANY certificate will be accepted, regardless of its validity /// - Expired certificates will be accepted /// - Certificates from untrusted issuers will be accepted /// - Certificates for the wrong domain will be accepted - /// + /// /// This is equivalent to calling `insecure_accept_invalid_certs(true)` on the builder /// and inherits all of its security implications. See that method's documentation /// for more details. - /// + /// /// # Intended Use Cases - /// + /// /// This method should ONLY be used for: /// - Local development with self-signed certificates /// - Testing environments where security is not a concern /// - Debugging TLS connection issues - /// + /// /// # Implementation Details - /// + /// /// This is a convenience wrapper that calls: /// ```ignore /// HttpClient::builder() @@ -300,12 +300,12 @@ impl HttpClient { /// # async { /// use hyper_custom_cert::{HttpClient, RequestOptions}; /// use std::collections::HashMap; - /// + /// /// let client = HttpClient::new(); - /// + /// /// // Basic request with no custom options /// let response1 = client.request_with_options("https://example.com", None).await?; - /// + /// /// // Request with custom options /// let mut headers = HashMap::new(); /// headers.insert("x-request-id".into(), "abc123".into()); @@ -318,7 +318,7 @@ impl HttpClient { pub async fn request(&self, url: &str) -> Result { self.request_with_options(url, None).await } - + /// Performs a GET request and returns the raw response body. /// This method constructs a `hyper::Request` with the GET method and any /// default headers configured on the client, then dispatches it via `perform_request`. @@ -335,12 +335,12 @@ impl HttpClient { /// # async { /// use hyper_custom_cert::{HttpClient, RequestOptions}; /// use std::collections::HashMap; - /// + /// /// let client = HttpClient::new(); - /// + /// /// // Basic request with no custom options /// let response1 = client.request_with_options("https://example.com", None).await?; - /// + /// /// // Request with custom options /// let mut headers = HashMap::new(); /// headers.insert("x-request-id".into(), "abc123".into()); @@ -349,13 +349,15 @@ impl HttpClient { /// # Ok::<(), hyper_custom_cert::ClientError>(()) /// # }; /// ``` - pub async fn request_with_options(&self, url: &str, options: Option) -> Result { + pub async fn request_with_options( + &self, + url: &str, + options: Option, + ) -> Result { let uri: Uri = url.parse()?; - - let req = Request::builder() - .method(Method::GET) - .uri(uri); - + + let req = Request::builder().method(Method::GET).uri(uri); + // Add default headers to the request. This ensures that any headers // set during the client's construction (e.g., API keys, User-Agent) // are automatically included in outgoing requests. @@ -363,7 +365,7 @@ impl HttpClient { for (key, value) in &self.default_headers { req = req.header(key, value); } - + // Add any request-specific headers from options if let Some(options) = &options { if let Some(headers) = &options.headers { @@ -372,9 +374,9 @@ impl HttpClient { } } } - + let req = req.body(http_body_util::Empty::::new())?; - + // If options contain a timeout, temporarily modify self to use it // This is a bit of a hack since we can't modify perform_request easily let result = if let Some(opts) = &options { @@ -389,7 +391,7 @@ impl HttpClient { #[cfg(feature = "rustls")] pinned_cert_sha256: self.pinned_cert_sha256.clone(), }; - + // Use the modified client for this request only client_copy.perform_request(req).await } else { @@ -400,7 +402,7 @@ impl HttpClient { // No options, use normal client self.perform_request(req).await }; - + result } @@ -423,12 +425,12 @@ impl HttpClient { /// use hyper_custom_cert::{HttpClient, RequestOptions}; /// use std::collections::HashMap; /// use std::time::Duration; - /// + /// /// let client = HttpClient::new(); - /// + /// /// // Basic POST request with no custom options /// let response1 = client.post_with_options("https://example.com/api", b"{\"key\":\"value\"}", None).await?; - /// + /// /// // POST request with custom options /// let mut headers = HashMap::new(); /// headers.insert("Content-Type".into(), "application/json".into()); @@ -439,11 +441,18 @@ impl HttpClient { /// # Ok::<(), hyper_custom_cert::ClientError>(()) /// # }; /// ``` - #[deprecated(since = "0.4.0", note = "Use post_with_options(url, body, Some(options)) instead")] - pub async fn post>(&self, url: &str, body: B) -> Result { + #[deprecated( + since = "0.4.0", + note = "Use post_with_options(url, body, Some(options)) instead" + )] + pub async fn post>( + &self, + url: &str, + body: B, + ) -> Result { self.post_with_options(url, body, None).await } - + /// Performs a POST request with the given body and returns the raw response. /// Similar to `request`, this method builds a `hyper::Request` for a POST /// operation, handles the request body conversion to `Bytes`, and applies @@ -463,12 +472,12 @@ impl HttpClient { /// use hyper_custom_cert::{HttpClient, RequestOptions}; /// use std::collections::HashMap; /// use std::time::Duration; - /// + /// /// let client = HttpClient::new(); - /// + /// /// // Basic POST request with no custom options /// let response1 = client.post_with_options("https://example.com/api", b"{\"key\":\"value\"}", None).await?; - /// + /// /// // POST request with custom options /// let mut headers = HashMap::new(); /// headers.insert("Content-Type".into(), "application/json".into()); @@ -479,19 +488,22 @@ impl HttpClient { /// # Ok::<(), hyper_custom_cert::ClientError>(()) /// # }; /// ``` - pub async fn post_with_options>(&self, url: &str, body: B, options: Option) -> Result { + pub async fn post_with_options>( + &self, + url: &str, + body: B, + options: Option, + ) -> Result { let uri: Uri = url.parse()?; - - let req = Request::builder() - .method(Method::POST) - .uri(uri); - + + let req = Request::builder().method(Method::POST).uri(uri); + // Add default headers to the request for consistency across client operations. let mut req = req; for (key, value) in &self.default_headers { req = req.header(key, value); } - + // Add any request-specific headers from options if let Some(options) = &options { if let Some(headers) = &options.headers { @@ -500,10 +512,10 @@ impl HttpClient { } } } - + let body_bytes = Bytes::copy_from_slice(body.as_ref()); let req = req.body(http_body_util::Full::new(body_bytes))?; - + // If options contain a timeout, temporarily modify self to use it // This is a bit of a hack since we can't modify perform_request easily let result = if let Some(opts) = &options { @@ -518,7 +530,7 @@ impl HttpClient { #[cfg(feature = "rustls")] pinned_cert_sha256: self.pinned_cert_sha256.clone(), }; - + // Use the modified client for this request only client_copy.perform_request(req).await } else { @@ -529,7 +541,7 @@ impl HttpClient { // No options, use normal client self.perform_request(req).await }; - + result } @@ -537,7 +549,7 @@ impl HttpClient { /// This centralizes the logic for dispatching `hyper::Request` objects, /// handling the various TLS backends (native-tls, rustls) and ensuring /// the correct `hyper` client is used based on feature flags. - async fn perform_request(&self, req: Request) -> Result + async fn perform_request(&self, req: Request) -> Result where B: hyper::body::Body + Send + 'static + Unpin, B::Data: Send, @@ -547,46 +559,44 @@ impl HttpClient { { // When the "native-tls" feature is enabled, use `hyper-tls` for TLS // support, which integrates with the system's native TLS libraries. - + #[cfg(feature = "insecure-dangerous")] if self.accept_invalid_certs { // ⚠️ SECURITY WARNING: This code path deliberately bypasses TLS certificate validation. // It should only be used during development/testing with self-signed certificates, - // and NEVER in production environments. This creates a vulnerability to + // and NEVER in production environments. This creates a vulnerability to // man-in-the-middle attacks and is extremely dangerous. - + // Implementation with tokio-native-tls to accept invalid certificates let mut http_connector = hyper_util::client::legacy::connect::HttpConnector::new(); http_connector.enforce_http(false); - + // Create a TLS connector that accepts invalid certificates let mut tls_builder = native_tls::TlsConnector::builder(); tls_builder.danger_accept_invalid_certs(true); - let tls_connector = tls_builder.build() - .map_err(|e| ClientError::TlsError(format!("Failed to build TLS connector: {}", e)))?; - + let tls_connector = tls_builder.build().map_err(|e| { + ClientError::TlsError(format!("Failed to build TLS connector: {}", e)) + })?; + // Create the tokio-native-tls connector let tokio_connector = tokio_native_tls::TlsConnector::from(tls_connector); - + // Create the HTTPS connector using the HTTP and TLS connectors let connector = hyper_tls::HttpsConnector::from((http_connector, tokio_connector)); - - let client = Client::builder(TokioExecutor::new()) - .build(connector); + + let client = Client::builder(TokioExecutor::new()).build(connector); let resp = tokio::time::timeout(self.timeout, client.request(req)) .await - .map_err(|_| ClientError::TlsError("Request timed out".to_string()))? - ?; + .map_err(|_| ClientError::TlsError("Request timed out".to_string()))??; return self.build_response(resp).await; } - + // Standard secure TLS connection with certificate validation (default path) let connector = hyper_tls::HttpsConnector::new(); let client = Client::builder(TokioExecutor::new()).build(connector); let resp = tokio::time::timeout(self.timeout, client.request(req)) .await - .map_err(|_| ClientError::TlsError("Request timed out".to_string()))? - ?; + .map_err(|_| ClientError::TlsError("Request timed out".to_string()))??; self.build_response(resp).await } #[cfg(all(feature = "rustls", not(feature = "native-tls")))] @@ -594,58 +604,70 @@ impl HttpClient { // If "rustls" is enabled and "native-tls" is not, use `rustls` for TLS. // Properly configure the rustls connector with custom CA certificates and/or // certificate validation settings based on the client configuration. - + // Start with the standard rustls config with native roots let mut root_cert_store = rustls::RootCertStore::empty(); - + // Load native certificates using rustls_native_certs v0.8.1 // This returns a CertificateResult which has a certs field containing the certificates let native_certs = rustls_native_certs::load_native_certs(); - + // Add each cert to the root store for cert in &native_certs.certs { if let Err(e) = root_cert_store.add(cert.clone()) { - return Err(ClientError::TlsError(format!("Failed to add native cert to root store: {}", e))); + return Err(ClientError::TlsError(format!( + "Failed to add native cert to root store: {}", + e + ))); } } - + // Add custom CA certificate if provided if let Some(ref pem_bytes) = self.root_ca_pem { let mut reader = std::io::Cursor::new(pem_bytes); for cert_result in rustls_pemfile::certs(&mut reader) { match cert_result { Ok(cert) => { - root_cert_store.add(cert) - .map_err(|e| ClientError::TlsError(format!("Failed to add custom cert to root store: {}", e)))?; - }, - Err(e) => return Err(ClientError::TlsError(format!("Failed to parse PEM cert: {}", e))), + root_cert_store.add(cert).map_err(|e| { + ClientError::TlsError(format!( + "Failed to add custom cert to root store: {}", + e + )) + })?; + } + Err(e) => { + return Err(ClientError::TlsError(format!( + "Failed to parse PEM cert: {}", + e + ))); + } } } } - + // Configure rustls - let mut config_builder = rustls::ClientConfig::builder() - .with_root_certificates(root_cert_store); - + let mut config_builder = + rustls::ClientConfig::builder().with_root_certificates(root_cert_store); + let rustls_config = config_builder.with_no_client_auth(); - + #[cfg(feature = "insecure-dangerous")] let rustls_config = if self.accept_invalid_certs { // ⚠️ SECURITY WARNING: This code path deliberately bypasses TLS certificate validation. // It should only be used during development/testing with self-signed certificates, - // and NEVER in production environments. This creates a vulnerability to + // and NEVER in production environments. This creates a vulnerability to // man-in-the-middle attacks and is extremely dangerous. - - use std::sync::Arc; - use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified}; + use rustls::DigitallySignedStruct; use rustls::SignatureScheme; + use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified}; use rustls::pki_types::UnixTime; - + use std::sync::Arc; + // Override the certificate verifier with a no-op verifier that accepts all certificates #[derive(Debug)] struct NoCertificateVerification {} - + impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification { fn verify_server_cert( &self, @@ -658,7 +680,7 @@ impl HttpClient { // Accept any certificate without verification Ok(ServerCertVerified::assertion()) } - + fn verify_tls12_signature( &self, _message: &[u8], @@ -668,7 +690,7 @@ impl HttpClient { // Accept any TLS 1.2 signature without verification Ok(HandshakeSignatureValid::assertion()) } - + fn verify_tls13_signature( &self, _message: &[u8], @@ -678,7 +700,7 @@ impl HttpClient { // Accept any TLS 1.3 signature without verification Ok(HandshakeSignatureValid::assertion()) } - + fn supported_verify_schemes(&self) -> Vec { // Return a list of all supported signature schemes vec![ @@ -698,31 +720,35 @@ impl HttpClient { ] } } - + // Set up the dangerous configuration with no certificate verification let mut config = rustls_config.clone(); - config.dangerous().set_certificate_verifier(Arc::new(NoCertificateVerification {})); + config + .dangerous() + .set_certificate_verifier(Arc::new(NoCertificateVerification {})); config } else { rustls_config }; - + // Handle certificate pinning if configured #[cfg(feature = "rustls")] let rustls_config = if let Some(ref pins) = self.pinned_cert_sha256 { // Implement certificate pinning by creating a custom certificate verifier - use std::sync::Arc; - use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; use rustls::DigitallySignedStruct; use rustls::SignatureScheme; + use rustls::client::danger::{ + HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier, + }; use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; - + use std::sync::Arc; + // Create a custom certificate verifier that checks certificate pins struct CertificatePinner { pins: Vec<[u8; 32]>, inner: Arc, } - + impl ServerCertVerifier for CertificatePinner { fn verify_server_cert( &self, @@ -733,28 +759,36 @@ impl HttpClient { now: UnixTime, ) -> Result { // First, use the inner verifier to do standard verification - self.inner.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)?; - + self.inner.verify_server_cert( + end_entity, + intermediates, + server_name, + ocsp_response, + now, + )?; + // Then verify the pin - use sha2::{Sha256, Digest}; - + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); hasher.update(end_entity.as_ref()); let cert_hash = hasher.finalize(); - + // Check if the certificate hash matches any of our pins for pin in &self.pins { if pin[..] == cert_hash[..] { return Ok(ServerCertVerified::assertion()); } } - + // If we got here, none of the pins matched - Err(rustls::Error::General("Certificate pin verification failed".into())) + Err(rustls::Error::General( + "Certificate pin verification failed".into(), + )) } - + fn verify_tls12_signature( - &self, + &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct, @@ -762,7 +796,7 @@ impl HttpClient { // Delegate to inner verifier self.inner.verify_tls12_signature(message, cert, dss) } - + fn verify_tls13_signature( &self, message: &[u8], @@ -772,46 +806,50 @@ impl HttpClient { // Delegate to inner verifier self.inner.verify_tls13_signature(message, cert, dss) } - + fn supported_verify_schemes(&self) -> Vec { self.inner.supported_verify_schemes() } } - + // Create the certificate pinner with our pins and the default verifier let mut config = rustls_config.clone(); let default_verifier = rustls::client::WebPkiServerVerifier::builder() .with_root_certificates(root_cert_store.clone()) .build() - .map_err(|e| ClientError::TlsError(format!("Failed to build certificate verifier: {}", e)))?; - + .map_err(|e| { + ClientError::TlsError(format!( + "Failed to build certificate verifier: {}", + e + )) + })?; + let cert_pinner = Arc::new(CertificatePinner { pins: pins.clone(), inner: default_verifier, }); - + config.dangerous().set_certificate_verifier(cert_pinner); config } else { rustls_config }; - + // Create a connector that supports HTTP and HTTPS let mut http_connector = hyper_util::client::legacy::connect::HttpConnector::new(); http_connector.enforce_http(false); - + // Create the rustls connector using HttpsConnectorBuilder let https_connector = hyper_rustls::HttpsConnectorBuilder::new() .with_tls_config(rustls_config) .https_or_http() .enable_http1() .build(); - + let client = Client::builder(TokioExecutor::new()).build(https_connector); let resp = tokio::time::timeout(self.timeout, client.request(req)) .await - .map_err(|_| ClientError::TlsError("Request timed out".to_string()))? - ?; + .map_err(|_| ClientError::TlsError("Request timed out".to_string()))??; self.build_response(resp).await } #[cfg(not(any(feature = "native-tls", feature = "rustls")))] @@ -824,8 +862,7 @@ impl HttpClient { let client = Client::builder(TokioExecutor::new()).build(connector); let resp = tokio::time::timeout(self.timeout, client.request(req)) .await - .map_err(|_| ClientError::TlsError("Request timed out".to_string()))? - ?; + .map_err(|_| ClientError::TlsError("Request timed out".to_string()))??; self.build_response(resp).await } } @@ -836,7 +873,7 @@ impl HttpClient { /// response body into a `Bytes` buffer for easy consumption by the caller. async fn build_response(&self, resp: Response) -> Result { let status = resp.status(); - + // Convert hyper's `HeaderMap` to a `HashMap` for simpler // public API exposure, making header access more idiomatic for consumers. let mut headers = HashMap::new(); @@ -845,12 +882,12 @@ impl HttpClient { headers.insert(name.to_string(), value_str.to_string()); } } - + // Collect the body as raw bytes - this is the key part of the issue // We expose the body as raw bytes without any permutations, ensuring // the client receives the exact byte content of the response. let body_bytes = resp.into_body().collect().await?.to_bytes(); - + Ok(HttpResponse { status, headers, @@ -865,26 +902,41 @@ impl HttpClient { /// On wasm32 targets, runtime methods are stubbed and return /// `ClientError::WasmNotImplemented` because browsers do not allow /// programmatic installation/trust of custom CAs. - #[deprecated(since = "0.4.0", note = "Use request_with_options(url, Some(options)) instead")] + #[deprecated( + since = "0.4.0", + note = "Use request_with_options(url, Some(options)) instead" + )] pub fn request(&self, _url: &str) -> Result<(), ClientError> { Err(ClientError::WasmNotImplemented) } - + /// On wasm32 targets, runtime methods are stubbed and return /// `ClientError::WasmNotImplemented` because browsers do not allow /// programmatic installation/trust of custom CAs. - pub fn request_with_options(&self, _url: &str, _options: Option) -> Result<(), ClientError> { + pub fn request_with_options( + &self, + _url: &str, + _options: Option, + ) -> Result<(), ClientError> { Err(ClientError::WasmNotImplemented) } /// POST is also not implemented on wasm32 targets for the same reason. - #[deprecated(since = "0.4.0", note = "Use post_with_options(url, body, Some(options)) instead")] + #[deprecated( + since = "0.4.0", + note = "Use post_with_options(url, body, Some(options)) instead" + )] pub fn post>(&self, _url: &str, _body: B) -> Result<(), ClientError> { Err(ClientError::WasmNotImplemented) } - + /// POST is also not implemented on wasm32 targets for the same reason. - pub fn post_with_options>(&self, _url: &str, _body: B, _options: Option) -> Result<(), ClientError> { + pub fn post_with_options>( + &self, + _url: &str, + _body: B, + _options: Option, + ) -> Result<(), ClientError> { Err(ClientError::WasmNotImplemented) } } @@ -928,26 +980,26 @@ impl HttpClientBuilder { /// Dev-only: accept self-signed/invalid TLS certificates. Requires the /// `insecure-dangerous` feature to be enabled. NEVER enable this in production. - /// + /// /// # Security Warning - /// + /// /// ⚠️ CRITICAL SECURITY WARNING ⚠️ - /// + /// /// This method deliberately bypasses TLS certificate validation, which creates a /// serious security vulnerability to man-in-the-middle attacks. When enabled: - /// + /// /// - The client will accept ANY certificate, regardless of its validity /// - The client will accept expired certificates /// - The client will accept certificates from untrusted issuers /// - The client will accept certificates for the wrong domain - /// + /// /// This method should ONLY be used for: /// - Local development with self-signed certificates /// - Testing environments where security is not a concern /// - Debugging TLS connection issues - /// + /// /// # Implementation Details - /// + /// /// When enabled, this setting: /// - For `native-tls`: Uses `danger_accept_invalid_certs(true)` on the TLS connector /// - For `rustls`: Implements a custom `ServerCertVerifier` that accepts all certificates @@ -1162,7 +1214,7 @@ mod tests { } #[cfg(not(target_arch = "wasm32"))] - #[tokio::test] + #[tokio::test] async fn post_returns_ok_on_native() { let client = HttpClient::builder().build(); // Just test that the method can be called - don't actually make network requests in tests diff --git a/crates/hyper-custom-cert/tests/default_features.rs b/crates/hyper-custom-cert/tests/default_features.rs index c34c18f..838529d 100644 --- a/crates/hyper-custom-cert/tests/default_features.rs +++ b/crates/hyper-custom-cert/tests/default_features.rs @@ -97,7 +97,6 @@ fn default_client_static_method() { let _client = HttpClient::default(); } - #[tokio::test] async fn post_smoke_default() { // Smoke test for POST support with default features diff --git a/crates/hyper-custom-cert/tests/example_server_integration.rs b/crates/hyper-custom-cert/tests/example_server_integration.rs index c980c4e..5ac7547 100644 --- a/crates/hyper-custom-cert/tests/example_server_integration.rs +++ b/crates/hyper-custom-cert/tests/example_server_integration.rs @@ -25,7 +25,7 @@ async fn test_default_client_against_example_endpoints() { // let _response = client.request("http://localhost:8080/health").await.unwrap(); // let _response = client.request("http://localhost:8080/status").await.unwrap(); // let _response = client.request("http://localhost:8080/test/client/default").await.unwrap(); - + // For testing purposes, just verify the client exists let _ = client; } @@ -55,13 +55,14 @@ fn test_timeout_configuration_for_example_server() { fn test_headers_configuration_for_example_server() { // Test custom headers configuration let mut headers = HashMap::new(); - headers.insert("User-Agent".to_string(), "hyper-custom-cert-integration-test/1.0".to_string()); + headers.insert( + "User-Agent".to_string(), + "hyper-custom-cert-integration-test/1.0".to_string(), + ); headers.insert("X-Test-Client".to_string(), "integration".to_string()); headers.insert("Accept".to_string(), "application/json".to_string()); - let client = HttpClient::builder() - .with_default_headers(headers) - .build(); + let client = HttpClient::builder().with_default_headers(headers).build(); // Smoke test - verify header configuration compiles let _ = client; @@ -71,7 +72,10 @@ fn test_headers_configuration_for_example_server() { fn test_combined_configuration_for_example_server() { // Test combining multiple configuration options let mut headers = HashMap::new(); - headers.insert("User-Agent".to_string(), "hyper-custom-cert-combined-test/1.0".to_string()); + headers.insert( + "User-Agent".to_string(), + "hyper-custom-cert-combined-test/1.0".to_string(), + ); let client = HttpClient::builder() .with_timeout(Duration::from_secs(30)) @@ -132,9 +136,7 @@ fn test_rustls_cert_pinning_configuration() { let dummy_pin = [0u8; 32]; let pins = vec![dummy_pin]; - let client = HttpClient::builder() - .with_pinned_cert_sha256(pins) - .build(); + let client = HttpClient::builder().with_pinned_cert_sha256(pins).build(); // Smoke test - verify cert pinning compiles let _ = client; @@ -171,18 +173,18 @@ async fn test_request_options() { use hyper_custom_cert::RequestOptions; use std::collections::HashMap; use std::time::Duration; - + // Test RequestOptions functionality let client = HttpClient::new(); - + // Create request options let mut headers = HashMap::new(); headers.insert("X-Custom-Header".to_string(), "test-value".to_string()); - + let options = RequestOptions::new() .with_headers(headers) .with_timeout(Duration::from_secs(15)); - + // Smoke test - verify request options can be used with both GET and POST // In real usage: // let _get_resp = client.request("https://example.com", Some(options.clone())).await.unwrap(); @@ -382,4 +384,4 @@ async fn test_insecure_usage_example() { // In real usage: let _response = client.request("https://localhost:8080/self-signed").await.unwrap(); // In real usage: let _response = client2.request("https://localhost:8080/self-signed").await.unwrap(); let _ = (client, client2); -} \ No newline at end of file +}