From cc54049c310776aa9530fcca194f2d2091ea7fcf Mon Sep 17 00:00:00 2001 From: Burak T Date: Mon, 10 Nov 2025 19:43:38 -0500 Subject: [PATCH 1/2] Add hyper-rustls HTTP client feature for docker/musl builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a new rustls-based HTTP client for web push notifications, ideal for docker/musl builds that don't require native-tls. Uses hyper-rustls v0.24 with minimal dependencies (http1, native-tokio). Follows existing design patterns with zero breaking changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Cargo.toml | 2 + src/clients/hyper_rustls_client.rs | 104 +++++++++++++++++++++++++++++ src/clients/mod.rs | 3 + src/error.rs | 2 +- src/lib.rs | 7 +- 5 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 src/clients/hyper_rustls_client.rs diff --git a/Cargo.toml b/Cargo.toml index 63d9f1ad..ef407170 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,10 +17,12 @@ edition = "2021" default = ["isahc-client"] isahc-client = ["isahc", "futures-lite/futures-io"] hyper-client = ["hyper", "hyper-tls"] # use features = ["hyper-client"], default-features = false for about 300kb size decrease +hyper-rustls-client = ["hyper", "hyper-rustls"] [dependencies] hyper = { version = "0.14", features = ["client", "http1"], optional = true } hyper-tls = { version = "0.5", optional = true } +hyper-rustls = { version = "0.24", default-features = false, features = ["http1", "native-tokio"], optional = true } isahc = { version = "1.4.0", optional = true } futures-lite = { version = "2.5.0", optional = true } http = "0.2" diff --git a/src/clients/hyper_rustls_client.rs b/src/clients/hyper_rustls_client.rs new file mode 100644 index 00000000..6f1faa7c --- /dev/null +++ b/src/clients/hyper_rustls_client.rs @@ -0,0 +1,104 @@ +use async_trait::async_trait; +use http::header::RETRY_AFTER; +use hyper::{body::HttpBody, client::HttpConnector, Body, Client, Request as HttpRequest}; +use hyper_rustls::HttpsConnector; + +use crate::{ + clients::{request_builder, WebPushClient, MAX_RESPONSE_SIZE}, + error::{RetryAfter, WebPushError}, + message::WebPushMessage, +}; + +/// An async client for sending the notification payload using rustls for TLS. +/// +/// This client is thread-safe. Clones of this client will share the same underlying resources, +/// so cloning is a cheap and effective method to provide access to the client. +/// +/// This client is [`hyper`](https://crates.io/crates/hyper) based with [`rustls`](https://crates.io/crates/rustls) +/// for TLS, and will only work in Tokio contexts. This variant is ideal for docker/musl builds +/// that don't require native-tls. +#[derive(Clone)] +pub struct HyperRustlsWebPushClient { + client: Client>, +} + +impl Default for HyperRustlsWebPushClient { + fn default() -> Self { + Self::new() + } +} + +impl From>> for HyperRustlsWebPushClient { + /// Creates a new client from a custom hyper HTTP client with rustls connector. + fn from(client: Client>) -> Self { + Self { client } + } +} + +impl HyperRustlsWebPushClient { + /// Creates a new client with rustls for TLS. + pub fn new() -> Self { + let https = hyper_rustls::HttpsConnectorBuilder::new() + .with_native_roots() + .https_or_http() + .enable_http1() + .build(); + + Self { + client: Client::builder().build(https), + } + } +} + +#[async_trait] +impl WebPushClient for HyperRustlsWebPushClient { + /// Sends a notification. Never times out. + async fn send(&self, message: WebPushMessage) -> Result<(), WebPushError> { + trace!("Message: {:?}", message); + + let request: HttpRequest = request_builder::build_request(message); + + debug!("Request: {:?}", request); + + let requesting = self.client.request(request); + + let response = requesting.await?; + + trace!("Response: {:?}", response); + + let retry_after = response + .headers() + .get(RETRY_AFTER) + .and_then(|ra| ra.to_str().ok()) + .and_then(RetryAfter::from_str); + + let response_status = response.status(); + trace!("Response status: {}", response_status); + + let mut chunks = response.into_body(); + let mut body = Vec::new(); + while let Some(chunk) = chunks.data().await { + body.extend(&chunk?); + if body.len() > MAX_RESPONSE_SIZE { + return Err(WebPushError::ResponseTooLarge); + } + } + trace!("Body: {:?}", body); + + trace!("Body text: {:?}", std::str::from_utf8(&body)); + + let response = request_builder::parse_response(response_status, body.to_vec()); + + debug!("Response: {:?}", response); + + if let Err(WebPushError::ServerError { + retry_after: None, + info, + }) = response + { + Err(WebPushError::ServerError { retry_after, info }) + } else { + Ok(response?) + } + } +} diff --git a/src/clients/mod.rs b/src/clients/mod.rs index 87a8ff77..54c6eaa7 100644 --- a/src/clients/mod.rs +++ b/src/clients/mod.rs @@ -12,6 +12,9 @@ pub mod request_builder; #[cfg(feature = "hyper-client")] pub mod hyper_client; +#[cfg(feature = "hyper-rustls-client")] +pub mod hyper_rustls_client; + #[cfg(feature = "isahc-client")] pub mod isahc_client; diff --git a/src/error.rs b/src/error.rs index 61cb8c18..3d4a31ae 100644 --- a/src/error.rs +++ b/src/error.rs @@ -93,7 +93,7 @@ impl From for WebPushError { } } -#[cfg(feature = "hyper-client")] +#[cfg(any(feature = "hyper-client", feature = "hyper-rustls-client"))] impl From for WebPushError { fn from(_: hyper::Error) -> Self { Self::Unspecified diff --git a/src/lib.rs b/src/lib.rs index 91499f42..fbf5bfb5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,8 +2,9 @@ //! //! A library for creating and sending push notifications to a web browser. For //! content payload encryption it uses [RFC8188](https://datatracker.ietf.org/doc/html/rfc8188). -//! The client is asynchronous and can run on any executor. An optional [`hyper`](https://crates.io/crates/hyper) based client is -//! available with the feature `hyper-client`. +//! The client is asynchronous and can run on any executor. Optional [`hyper`](https://crates.io/crates/hyper) +//! based clients are available with the features `hyper-client` (using native-tls) and +//! `hyper-rustls-client` (using rustls, ideal for docker/musl builds). //! //! # Example //! @@ -50,6 +51,8 @@ extern crate serde_derive; #[cfg(feature = "hyper-client")] pub use crate::clients::hyper_client::HyperWebPushClient; +#[cfg(feature = "hyper-rustls-client")] +pub use crate::clients::hyper_rustls_client::HyperRustlsWebPushClient; #[cfg(feature = "isahc-client")] pub use crate::clients::isahc_client::IsahcWebPushClient; pub use crate::{ From 0915d43da434c22f887f4a4855190a9f04df5ab5 Mon Sep 17 00:00:00 2001 From: Burak Date: Tue, 11 Nov 2025 00:48:20 -0500 Subject: [PATCH 2/2] Replace ece with pure-Rust RustCrypto backend (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates ece dependency to use RustCrypto backend (via forked rust-ece), eliminating OpenSSL requirement for hyper-rustls-client feature. Provides 100% pure-Rust crypto stack with rustls TLS and RustCrypto encryption for docker/musl builds. All tests pass with new backend. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- Cargo.toml | 4 ++-- src/lib.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ef407170..3db73525 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ edition = "2021" default = ["isahc-client"] isahc-client = ["isahc", "futures-lite/futures-io"] hyper-client = ["hyper", "hyper-tls"] # use features = ["hyper-client"], default-features = false for about 300kb size decrease -hyper-rustls-client = ["hyper", "hyper-rustls"] +hyper-rustls-client = ["hyper", "hyper-rustls"] # pure-Rust TLS with RustCrypto, ideal for docker/musl builds [dependencies] hyper = { version = "0.14", features = ["client", "http1"], optional = true } @@ -30,7 +30,7 @@ serde = "1.0" serde_json = "1.0" serde_derive = "1.0" jwt-simple = { version = "0.12.11", default-features = false, features = ["pure-rust"] } -ece = "2.2" +ece = { git = "https://github.com/buraktabn/rust-ece", default-features = false, features = ["backend-rustcrypto", "serializable-keys"] } pem = "3.0.4" sec1_decode = "0.1.0" chrono = "0.4" diff --git a/src/lib.rs b/src/lib.rs index fbf5bfb5..c5e8802d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ //! content payload encryption it uses [RFC8188](https://datatracker.ietf.org/doc/html/rfc8188). //! The client is asynchronous and can run on any executor. Optional [`hyper`](https://crates.io/crates/hyper) //! based clients are available with the features `hyper-client` (using native-tls) and -//! `hyper-rustls-client` (using rustls, ideal for docker/musl builds). +//! `hyper-rustls-client` (using rustls + pure-Rust crypto, ideal for docker/musl builds). //! //! # Example //!