diff --git a/Cargo.toml b/Cargo.toml index 63d9f1ad..3db73525 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"] # pure-Rust TLS with RustCrypto, ideal for docker/musl builds [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" @@ -28,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/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..c5e8802d 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 + pure-Rust crypto, 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::{