diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index de46941..7c02fa9 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -34,7 +34,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.63.0 + toolchain: 1.64.0 - run: cargo check --lib --all-features lint: diff --git a/Cargo.toml b/Cargo.toml index bebbf6e..feb792e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "instant-acme" -version = "0.5.0" +version = "0.6.0" edition = "2021" -rust-version = "1.63" +rust-version = "1.64" license = "Apache-2.0" description = "Async pure-Rust ACME client" homepage = "https://github.com/instant-labs/instant-acme" @@ -16,8 +16,10 @@ default = ["hyper-rustls"] [dependencies] base64 = "0.21.0" -hyper = { version = "0.14.18", features = ["client", "http1", "http2"] } -hyper-rustls = { version = "0.24", default-features = false, features = ["http1", "http2", "native-tokio", "tls12"], optional = true } +hyper = { version = "1.3.1", features = ["client", "http1", "http2"] } +hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "tls12", "rustls-native-certs", "ring"], optional = true } +hyper-util = { version = "0.1.5", features = ["client", "client-legacy", "http1", "http2", "tokio"]} +http-body-util = "0.1.2" ring = { version = "0.17", features = ["std"] } rustls-pki-types = "1.1.0" serde = { version = "1.0.104", features = ["derive"] } diff --git a/deny.toml b/deny.toml index 0a8819d..a33d5ac 100644 --- a/deny.toml +++ b/deny.toml @@ -1,6 +1,6 @@ [licenses] version = 2 -allow = ["Apache-2.0", "ISC", "MIT", "OpenSSL", "Unicode-DFS-2016"] +allow = ["Apache-2.0", "BSD-3-Clause", "ISC", "MIT", "OpenSSL", "Unicode-DFS-2016"] [[licenses.clarify]] name = "ring" diff --git a/src/lib.rs b/src/lib.rs index 29b308f..2b7927b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,11 +9,14 @@ use std::pin::Pin; use std::sync::Arc; use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD}; -use hyper::client::connect::Connect; -#[cfg(feature = "hyper-rustls")] -use hyper::client::HttpConnector; +use http_body_util::{BodyExt, Full}; +use hyper::body::{Bytes, Incoming}; use hyper::header::{CONTENT_TYPE, LOCATION}; -use hyper::{Body, Method, Request, Response, StatusCode}; +use hyper::{Method, Request, Response, StatusCode}; +#[cfg(feature = "hyper-rustls")] +use hyper_util::client::legacy::connect::{Connect, HttpConnector}; +use hyper_util::client::legacy::Client as HyperClient; +use hyper_util::rt::TokioExecutor; use ring::digest::{digest, SHA256}; use ring::rand::SystemRandom; use ring::signature::{EcdsaKeyPair, ECDSA_P256_SHA256_FIXED_SIGNING}; @@ -134,7 +137,11 @@ impl Order { .await?; self.nonce = nonce_from_response(&rsp); - let body = hyper::body::to_bytes(Problem::from_response(rsp).await?).await?; + let body = Problem::from_response(rsp) + .await? + .collect() + .await? + .to_bytes(); Ok(Some( String::from_utf8(body.to_vec()) .map_err(|_| "unable to decode certificate as UTF-8")?, @@ -206,7 +213,7 @@ impl Account { pub async fn from_credentials(credentials: AccountCredentials) -> Result { Ok(Self { inner: Arc::new( - AccountInner::from_credentials(credentials, Box::::default()) + AccountInner::from_credentials(credentials, Box::new(DefaultClient::try_new()?)) .await?, ), }) @@ -256,7 +263,7 @@ impl Account { Self::create_inner( account, external_account, - Client::new(server_url, Box::::default()).await?, + Client::new(server_url, Box::new(DefaultClient::try_new()?)).await?, server_url, ) .await @@ -419,7 +426,7 @@ impl AccountInner { payload: Option<&impl Serialize>, nonce: Option, url: &str, - ) -> Result, Error> { + ) -> Result, Error> { self.client.post(payload, nonce, self, url).await } } @@ -451,10 +458,10 @@ impl Client { async fn new(server_url: &str, http: Box) -> Result { let req = Request::builder() .uri(server_url) - .body(Body::empty()) - .unwrap(); + .body(Full::default()) + .expect("infallible error should not occur"); let rsp = http.request(req).await?; - let body = hyper::body::to_bytes(rsp.into_body()).await?; + let body = rsp.into_body().collect().await?.to_bytes(); Ok(Client { http, urls: serde_json::from_slice(&body)?, @@ -467,17 +474,16 @@ impl Client { nonce: Option, signer: &impl Signer, url: &str, - ) -> Result, Error> { + ) -> Result, Error> { let nonce = self.nonce(nonce).await?; let body = JoseJson::new(payload, signer.header(Some(&nonce), url), signer)?; let request = Request::builder() .method(Method::POST) .uri(url) .header(CONTENT_TYPE, JOSE_JSON) - .body(Body::from(serde_json::to_vec(&body)?)) - .unwrap(); + .body(Full::from(serde_json::to_vec(&body)?))?; - Ok(self.http.request(request).await?) + self.http.request(request).await } async fn nonce(&self, nonce: Option) -> Result { @@ -488,8 +494,8 @@ impl Client { let request = Request::builder() .method(Method::HEAD) .uri(&self.urls.new_nonce) - .body(Body::empty()) - .unwrap(); + .body(Full::default()) + .expect("infallible error should not occur"); let rsp = self.http.request(request).await?; // https://datatracker.ietf.org/doc/html/rfc8555#section-7.2 @@ -650,38 +656,40 @@ impl Signer for ExternalAccountKey { } } -fn nonce_from_response(rsp: &Response) -> Option { +fn nonce_from_response(rsp: &Response) -> Option { rsp.headers() .get(REPLAY_NONCE) .and_then(|hv| String::from_utf8(hv.as_ref().to_vec()).ok()) } #[cfg(feature = "hyper-rustls")] -struct DefaultClient(hyper::Client>); +struct DefaultClient(HyperClient, Full>); #[cfg(feature = "hyper-rustls")] -impl HttpClient for DefaultClient { - fn request( - &self, - req: Request, - ) -> Pin>> + Send>> { - Box::pin(self.0.request(req)) - } -} - -#[cfg(feature = "hyper-rustls")] -impl Default for DefaultClient { - fn default() -> Self { - Self( - hyper::Client::builder().build( +impl DefaultClient { + fn try_new() -> Result { + Ok(Self( + HyperClient::builder(TokioExecutor::new()).build( hyper_rustls::HttpsConnectorBuilder::new() .with_native_roots() + .map_err(|e| Error::Other(Box::new(e)))? .https_only() .enable_http1() .enable_http2() .build(), ), - ) + )) + } +} + +#[cfg(feature = "hyper-rustls")] +impl HttpClient for DefaultClient { + fn request( + &self, + req: Request>, + ) -> Pin, Error>> + Send>> { + let fut = self.0.request(req); + Box::pin(async move { fut.await.map_err(Error::from) }) } } @@ -690,19 +698,20 @@ pub trait HttpClient: Send + Sync + 'static { /// Send the given request and return the response fn request( &self, - req: Request, - ) -> Pin>> + Send>>; + req: Request>, + ) -> Pin, Error>> + Send>>; } -impl HttpClient for hyper::Client +impl HttpClient for HyperClient> where C: Connect + Clone + Send + Sync + 'static, { fn request( &self, - req: Request, - ) -> Pin>> + Send>> { - Box::pin(>::request(self, req)) + req: Request>, + ) -> Pin, Error>> + Send>> { + let fut = >>::request(self, req); + Box::pin(async move { fut.await.map_err(Error::from) }) } } diff --git a/src/types.rs b/src/types.rs index dfd73dd..41da9b0 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,7 +1,9 @@ use std::fmt; use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD}; -use hyper::{Body, Response}; +use http_body_util::BodyExt; +use hyper::body::Incoming; +use hyper::Response; use ring::digest::{digest, Digest, SHA256}; use ring::signature::{EcdsaKeyPair, KeyPair}; use rustls_pki_types::CertificateDer; @@ -27,15 +29,21 @@ pub enum Error { /// Failed to instantiate a private key #[error("invalid key bytes: {0}")] CryptoKey(#[from] ring::error::KeyRejected), - /// HTTP request failure + /// HTTP failure #[error("HTTP request failure: {0}")] - Http(#[from] hyper::Error), + Http(#[from] hyper::http::Error), + /// Hyper request failure + #[error("HTTP request failure: {0}")] + Hyper(#[from] hyper::Error), /// Invalid ACME server URL #[error("invalid URI: {0}")] InvalidUri(#[from] hyper::http::uri::InvalidUri), /// Failed to (de)serialize a JSON object #[error("failed to (de)serialize JSON: {0}")] Json(#[from] serde_json::Error), + /// Other kind of error + #[error(transparent)] + Other(Box), /// Miscellaneous errors #[error("missing data: {0}")] Str(&'static str), @@ -47,6 +55,12 @@ impl From<&'static str> for Error { } } +impl From for Error { + fn from(value: hyper_util::client::legacy::Error) -> Self { + Self::Other(Box::new(value)) + } +} + /// ACME account credentials /// /// This opaque type contains the account ID, the private key data and the @@ -119,20 +133,20 @@ pub struct Problem { } impl Problem { - pub(crate) async fn check(rsp: Response) -> Result { + pub(crate) async fn check(rsp: Response) -> Result { Ok(serde_json::from_slice( - &hyper::body::to_bytes(Self::from_response(rsp).await?).await?, + &Self::from_response(rsp).await?.collect().await?.to_bytes(), )?) } - pub(crate) async fn from_response(rsp: Response) -> Result { + pub(crate) async fn from_response(rsp: Response) -> Result { let status = rsp.status(); let body = rsp.into_body(); if status.is_informational() || status.is_success() || status.is_redirection() { return Ok(body); } - let body = hyper::body::to_bytes(body).await?; + let body = body.collect().await?.to_bytes(); Err(serde_json::from_slice::(&body)?.into()) } }