Make HttpClient independent from hyper
This commit is contained in:
parent
2047a9d579
commit
126c0a3161
|
@ -15,17 +15,20 @@ categories = ["web-programming", "api-bindings"]
|
||||||
default = ["hyper-rustls", "ring"]
|
default = ["hyper-rustls", "ring"]
|
||||||
aws-lc-rs = ["dep:aws-lc-rs", "hyper-rustls?/aws-lc-rs", "rcgen/aws_lc_rs"]
|
aws-lc-rs = ["dep:aws-lc-rs", "hyper-rustls?/aws-lc-rs", "rcgen/aws_lc_rs"]
|
||||||
fips = ["aws-lc-rs", "aws-lc-rs?/fips"]
|
fips = ["aws-lc-rs", "aws-lc-rs?/fips"]
|
||||||
|
hyper-rustls = ["dep:hyper", "dep:hyper-rustls", "dep:hyper-util"]
|
||||||
ring = ["dep:ring", "hyper-rustls?/ring", "rcgen/ring"]
|
ring = ["dep:ring", "hyper-rustls?/ring", "rcgen/ring"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
async-trait = "0.1"
|
||||||
aws-lc-rs = { version = "1.8.0", optional = true }
|
aws-lc-rs = { version = "1.8.0", optional = true }
|
||||||
base64 = "0.21.0"
|
base64 = "0.21.0"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
http = "1"
|
http = "1"
|
||||||
|
http-body = "1"
|
||||||
http-body-util = "0.1.2"
|
http-body-util = "0.1.2"
|
||||||
hyper = { version = "1.3.1", features = ["client", "http1", "http2"] }
|
hyper = { version = "1.3.1", features = ["client", "http1", "http2"], optional = true }
|
||||||
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "tls12", "rustls-native-certs"], optional = true }
|
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "tls12", "rustls-native-certs"], optional = true }
|
||||||
hyper-util = { version = "0.1.5", features = ["client", "client-legacy", "http1", "http2", "tokio"] }
|
hyper-util = { version = "0.1.5", features = ["client", "client-legacy", "http1", "http2", "tokio"], optional = true }
|
||||||
ring = { version = "0.17", features = ["std"], optional = true }
|
ring = { version = "0.17", features = ["std"], optional = true }
|
||||||
rustls-pki-types = "1.1.0"
|
rustls-pki-types = "1.1.0"
|
||||||
serde = { version = "1.0.104", features = ["derive"] }
|
serde = { version = "1.0.104", features = ["derive"] }
|
||||||
|
|
117
src/lib.rs
117
src/lib.rs
|
@ -3,17 +3,21 @@
|
||||||
#![warn(unreachable_pub)]
|
#![warn(unreachable_pub)]
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
use std::error::Error as StdError;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
|
use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
|
||||||
|
use bytes::Bytes;
|
||||||
use http::header::{CONTENT_TYPE, LOCATION};
|
use http::header::{CONTENT_TYPE, LOCATION};
|
||||||
use http::{Method, Request, Response, StatusCode};
|
use http::{Method, Request, Response, StatusCode};
|
||||||
use http_body_util::{BodyExt, Full};
|
use http_body_util::{BodyExt, Full};
|
||||||
use hyper::body::{Bytes, Incoming};
|
#[cfg(feature = "hyper-rustls")]
|
||||||
use hyper_util::client::legacy::connect::Connect;
|
use hyper_util::client::legacy::connect::Connect;
|
||||||
|
#[cfg(feature = "hyper-rustls")]
|
||||||
use hyper_util::client::legacy::Client as HyperClient;
|
use hyper_util::client::legacy::Client as HyperClient;
|
||||||
#[cfg(feature = "hyper-rustls")]
|
#[cfg(feature = "hyper-rustls")]
|
||||||
use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
|
use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
|
||||||
|
@ -310,7 +314,8 @@ impl Account {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let account_url = rsp
|
let account_url = rsp
|
||||||
.headers()
|
.parts
|
||||||
|
.headers
|
||||||
.get(LOCATION)
|
.get(LOCATION)
|
||||||
.and_then(|hv| hv.to_str().ok())
|
.and_then(|hv| hv.to_str().ok())
|
||||||
.map(|s| s.to_owned());
|
.map(|s| s.to_owned());
|
||||||
|
@ -352,7 +357,8 @@ impl Account {
|
||||||
|
|
||||||
let nonce = nonce_from_response(&rsp);
|
let nonce = nonce_from_response(&rsp);
|
||||||
let order_url = rsp
|
let order_url = rsp
|
||||||
.headers()
|
.parts
|
||||||
|
.headers
|
||||||
.get(LOCATION)
|
.get(LOCATION)
|
||||||
.and_then(|hv| hv.to_str().ok())
|
.and_then(|hv| hv.to_str().ok())
|
||||||
.map(|s| s.to_owned());
|
.map(|s| s.to_owned());
|
||||||
|
@ -441,7 +447,7 @@ impl AccountInner {
|
||||||
payload: Option<&impl Serialize>,
|
payload: Option<&impl Serialize>,
|
||||||
nonce: Option<String>,
|
nonce: Option<String>,
|
||||||
url: &str,
|
url: &str,
|
||||||
) -> Result<Response<Incoming>, Error> {
|
) -> Result<BytesResponse, Error> {
|
||||||
self.client.post(payload, nonce, self, url).await
|
self.client.post(payload, nonce, self, url).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -476,7 +482,7 @@ impl Client {
|
||||||
.body(Full::default())
|
.body(Full::default())
|
||||||
.expect("infallible error should not occur");
|
.expect("infallible error should not occur");
|
||||||
let rsp = http.request(req).await?;
|
let rsp = http.request(req).await?;
|
||||||
let body = rsp.into_body().collect().await?.to_bytes();
|
let body = rsp.body().await.map_err(Error::Other)?;
|
||||||
Ok(Client {
|
Ok(Client {
|
||||||
http,
|
http,
|
||||||
urls: serde_json::from_slice(&body)?,
|
urls: serde_json::from_slice(&body)?,
|
||||||
|
@ -489,7 +495,7 @@ impl Client {
|
||||||
nonce: Option<String>,
|
nonce: Option<String>,
|
||||||
signer: &impl Signer,
|
signer: &impl Signer,
|
||||||
url: &str,
|
url: &str,
|
||||||
) -> Result<Response<Incoming>, Error> {
|
) -> Result<BytesResponse, Error> {
|
||||||
let nonce = self.nonce(nonce).await?;
|
let nonce = self.nonce(nonce).await?;
|
||||||
let body = JoseJson::new(payload, signer.header(Some(&nonce), url), signer)?;
|
let body = JoseJson::new(payload, signer.header(Some(&nonce), url), signer)?;
|
||||||
let request = Request::builder()
|
let request = Request::builder()
|
||||||
|
@ -516,7 +522,7 @@ impl Client {
|
||||||
// https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
|
// https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
|
||||||
// "The server's response MUST include a Replay-Nonce header field containing a fresh
|
// "The server's response MUST include a Replay-Nonce header field containing a fresh
|
||||||
// nonce and SHOULD have status code 200 (OK)."
|
// nonce and SHOULD have status code 200 (OK)."
|
||||||
if rsp.status() != StatusCode::OK {
|
if rsp.parts.status != StatusCode::OK {
|
||||||
return Err("error response from newNonce resource".into());
|
return Err("error response from newNonce resource".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -663,8 +669,9 @@ impl Signer for ExternalAccountKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nonce_from_response(rsp: &Response<Incoming>) -> Option<String> {
|
fn nonce_from_response(rsp: &BytesResponse) -> Option<String> {
|
||||||
rsp.headers()
|
rsp.parts
|
||||||
|
.headers
|
||||||
.get(REPLAY_NONCE)
|
.get(REPLAY_NONCE)
|
||||||
.and_then(|hv| String::from_utf8(hv.as_ref().to_vec()).ok())
|
.and_then(|hv| String::from_utf8(hv.as_ref().to_vec()).ok())
|
||||||
}
|
}
|
||||||
|
@ -694,32 +701,102 @@ impl HttpClient for DefaultClient {
|
||||||
fn request(
|
fn request(
|
||||||
&self,
|
&self,
|
||||||
req: Request<Full<Bytes>>,
|
req: Request<Full<Bytes>>,
|
||||||
) -> Pin<Box<dyn Future<Output = Result<Response<Incoming>, Error>> + Send>> {
|
) -> Pin<Box<dyn Future<Output = Result<BytesResponse, Error>> + Send>> {
|
||||||
let fut = self.0.request(req);
|
let fut = self.0.request(req);
|
||||||
Box::pin(async move { fut.await.map_err(Error::from) })
|
Box::pin(async move {
|
||||||
|
match fut.await {
|
||||||
|
Ok(rsp) => Ok(BytesResponse::from(rsp)),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A HTTP client based on [`hyper::Client`]
|
/// A HTTP client abstraction
|
||||||
pub trait HttpClient: Send + Sync + 'static {
|
pub trait HttpClient: Send + Sync + 'static {
|
||||||
/// Send the given request and return the response
|
/// Send the given request and return the response
|
||||||
fn request(
|
fn request(
|
||||||
&self,
|
&self,
|
||||||
req: Request<Full<Bytes>>,
|
req: Request<Full<Bytes>>,
|
||||||
) -> Pin<Box<dyn Future<Output = Result<Response<Incoming>, Error>> + Send>>;
|
) -> Pin<Box<dyn Future<Output = Result<BytesResponse, Error>> + Send>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C> HttpClient for HyperClient<C, Full<Bytes>>
|
#[cfg(feature = "hyper-rustls")]
|
||||||
where
|
impl<C: Connect + Clone + Send + Sync + 'static> HttpClient for HyperClient<C, Full<Bytes>> {
|
||||||
C: Connect + Clone + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
fn request(
|
fn request(
|
||||||
&self,
|
&self,
|
||||||
req: Request<Full<Bytes>>,
|
req: Request<Full<Bytes>>,
|
||||||
) -> Pin<Box<dyn Future<Output = Result<Response<hyper::body::Incoming>, Error>> + Send>> {
|
) -> Pin<Box<dyn Future<Output = Result<BytesResponse, Error>> + Send>> {
|
||||||
let fut = <HyperClient<C, Full<Bytes>>>::request(self, req);
|
let fut = self.request(req);
|
||||||
Box::pin(async move { fut.await.map_err(Error::from) })
|
Box::pin(async move {
|
||||||
|
match fut.await {
|
||||||
|
Ok(rsp) => Ok(BytesResponse::from(rsp)),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response with object safe body type
|
||||||
|
pub struct BytesResponse {
|
||||||
|
/// Response status and header
|
||||||
|
pub parts: http::response::Parts,
|
||||||
|
/// Response body
|
||||||
|
pub body: Box<dyn BytesBody>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BytesResponse {
|
||||||
|
pub(crate) async fn body(mut self) -> Result<Bytes, Box<dyn StdError + Send + Sync + 'static>> {
|
||||||
|
self.body.into_bytes().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B> From<Response<B>> for BytesResponse
|
||||||
|
where
|
||||||
|
B: http_body::Body + Send + Unpin + 'static,
|
||||||
|
B::Data: Send,
|
||||||
|
B::Error: Into<Box<dyn StdError + Send + Sync + 'static>>,
|
||||||
|
{
|
||||||
|
fn from(rsp: Response<B>) -> Self {
|
||||||
|
let (parts, body) = rsp.into_parts();
|
||||||
|
Self {
|
||||||
|
parts,
|
||||||
|
body: Box::new(BodyWrapper { inner: Some(body) }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BodyWrapper<B> {
|
||||||
|
inner: Option<B>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<B> BytesBody for BodyWrapper<B>
|
||||||
|
where
|
||||||
|
B: http_body::Body + Send + Unpin + 'static,
|
||||||
|
B::Data: Send,
|
||||||
|
B::Error: Into<Box<dyn StdError + Send + Sync + 'static>>,
|
||||||
|
{
|
||||||
|
async fn into_bytes(&mut self) -> Result<Bytes, Box<dyn StdError + Send + Sync + 'static>> {
|
||||||
|
let Some(body) = self.inner.take() else {
|
||||||
|
return Ok(Bytes::new());
|
||||||
|
};
|
||||||
|
|
||||||
|
match body.collect().await {
|
||||||
|
Ok(body) => Ok(body.to_bytes()),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Object safe body trait
|
||||||
|
#[async_trait]
|
||||||
|
pub trait BytesBody {
|
||||||
|
/// Convert the body into [`Bytes`]
|
||||||
|
///
|
||||||
|
/// This consumes the body. The behavior for calling this method multiple times is undefined.
|
||||||
|
#[allow(clippy::wrong_self_convention)] // async_trait doesn't support taking `self`
|
||||||
|
async fn into_bytes(&mut self) -> Result<Bytes, Box<dyn StdError + Send + Sync + 'static>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
mod crypto {
|
mod crypto {
|
||||||
|
|
14
src/types.rs
14
src/types.rs
|
@ -2,9 +2,6 @@ use std::fmt;
|
||||||
|
|
||||||
use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
|
use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use http_body_util::BodyExt;
|
|
||||||
use hyper::body::Incoming;
|
|
||||||
use hyper::Response;
|
|
||||||
use rustls_pki_types::CertificateDer;
|
use rustls_pki_types::CertificateDer;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::ser::SerializeMap;
|
use serde::ser::SerializeMap;
|
||||||
|
@ -12,6 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::crypto::{self, KeyPair};
|
use crate::crypto::{self, KeyPair};
|
||||||
|
use crate::BytesResponse;
|
||||||
|
|
||||||
/// Error type for instant-acme
|
/// Error type for instant-acme
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
@ -34,6 +32,7 @@ pub enum Error {
|
||||||
#[error("HTTP request failure: {0}")]
|
#[error("HTTP request failure: {0}")]
|
||||||
Http(#[from] http::Error),
|
Http(#[from] http::Error),
|
||||||
/// Hyper request failure
|
/// Hyper request failure
|
||||||
|
#[cfg(feature = "hyper-rustls")]
|
||||||
#[error("HTTP request failure: {0}")]
|
#[error("HTTP request failure: {0}")]
|
||||||
Hyper(#[from] hyper::Error),
|
Hyper(#[from] hyper::Error),
|
||||||
/// Invalid ACME server URL
|
/// Invalid ACME server URL
|
||||||
|
@ -56,6 +55,7 @@ impl From<&'static str> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hyper-rustls")]
|
||||||
impl From<hyper_util::client::legacy::Error> for Error {
|
impl From<hyper_util::client::legacy::Error> for Error {
|
||||||
fn from(value: hyper_util::client::legacy::Error) -> Self {
|
fn from(value: hyper_util::client::legacy::Error) -> Self {
|
||||||
Self::Other(Box::new(value))
|
Self::Other(Box::new(value))
|
||||||
|
@ -134,13 +134,13 @@ pub struct Problem {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Problem {
|
impl Problem {
|
||||||
pub(crate) async fn check<T: DeserializeOwned>(rsp: Response<Incoming>) -> Result<T, Error> {
|
pub(crate) async fn check<T: DeserializeOwned>(rsp: BytesResponse) -> Result<T, Error> {
|
||||||
Ok(serde_json::from_slice(&Self::from_response(rsp).await?)?)
|
Ok(serde_json::from_slice(&Self::from_response(rsp).await?)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn from_response(rsp: Response<Incoming>) -> Result<Bytes, Error> {
|
pub(crate) async fn from_response(rsp: BytesResponse) -> Result<Bytes, Error> {
|
||||||
let status = rsp.status();
|
let status = rsp.parts.status;
|
||||||
let body = rsp.into_body().collect().await?.to_bytes();
|
let body = rsp.body().await.map_err(Error::Other)?;
|
||||||
match status.is_informational() || status.is_success() || status.is_redirection() {
|
match status.is_informational() || status.is_success() || status.is_redirection() {
|
||||||
true => Ok(body),
|
true => Ok(body),
|
||||||
false => Err(serde_json::from_slice::<Problem>(&body)?.into()),
|
false => Err(serde_json::from_slice::<Problem>(&body)?.into()),
|
||||||
|
|
Loading…
Reference in New Issue