acme: switch from reqwest to hyper

This commit is contained in:
Dirkjan Ochtman 2022-05-10 14:56:42 +02:00 committed by Dirkjan Ochtman
parent 3e64360c59
commit 40a561ffb5
3 changed files with 59 additions and 35 deletions

View File

@ -6,7 +6,8 @@ publish = false
[dependencies] [dependencies]
base64 = "0.13.0" base64 = "0.13.0"
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls-native-roots"] } hyper = { version = "0.14.18", features = ["client", "http1", "http2"] }
hyper-rustls = { version = "0.23.0", default-features = false, features = ["http1", "http2", "native-tokio", "tls12"] }
ring = { version = "0.16.20", features = ["std"] } ring = { version = "0.16.20", features = ["std"] }
serde = { version = "1.0.104", features = ["derive"] } serde = { version = "1.0.104", features = ["derive"] }
serde_json = "1.0.78" serde_json = "1.0.78"

View File

@ -3,9 +3,9 @@
use std::sync::Arc; use std::sync::Arc;
use base64::URL_SAFE_NO_PAD; use base64::URL_SAFE_NO_PAD;
use reqwest::header::{CONTENT_TYPE, LOCATION}; use hyper::client::HttpConnector;
use reqwest::redirect::Policy; use hyper::header::{CONTENT_TYPE, LOCATION};
use reqwest::{Body, Response}; use hyper::{Body, Method, Request, Response};
use ring::rand::SystemRandom; use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, ECDSA_P256_SHA256_FIXED_SIGNING}; use ring::signature::{EcdsaKeyPair, ECDSA_P256_SHA256_FIXED_SIGNING};
use serde::de::{DeserializeOwned, Error as _, Unexpected}; use serde::de::{DeserializeOwned, Error as _, Unexpected};
@ -67,11 +67,11 @@ impl Order {
.await?; .await?;
self.nonce = nonce_from_response(&rsp); self.nonce = nonce_from_response(&rsp);
let status = rsp.status(); let body = hyper::body::to_bytes(Problem::from_response(rsp).await?).await?;
match status.is_client_error() || status.is_server_error() { Ok(
false => Ok(rsp.text().await?), String::from_utf8(body.to_vec())
true => Err(rsp.json::<Problem>().await?.into()), .map_err(|_| "unable to decode certificate as UTF-8")?,
} )
} }
pub async fn set_challenge_ready(&mut self, challenge_url: &str) -> Result<(), Error> { pub async fn set_challenge_ready(&mut self, challenge_url: &str) -> Result<(), Error> {
@ -113,7 +113,8 @@ impl Account {
.and_then(|hv| hv.to_str().ok()) .and_then(|hv| hv.to_str().ok())
.map(|s| s.to_owned()); .map(|s| s.to_owned());
Problem::from_response(rsp).await?; // The response redirects, we don't need the body
let _ = Problem::from_response(rsp).await?;
Ok(Self { Ok(Self {
inner: Arc::new(AccountInner { inner: Arc::new(AccountInner {
client, client,
@ -173,7 +174,7 @@ impl<'de> Deserialize<'de> for Account {
) )
})?, })?,
client: Client { client: Client {
client: client().map_err(D::Error::custom)?, client: client(),
urls: creds.urls.clone(), urls: creds.urls.clone(),
}, },
id: creds.id.clone(), id: creds.id.clone(),
@ -218,7 +219,7 @@ impl AccountInner {
payload: Option<&impl Serialize>, payload: Option<&impl Serialize>,
nonce: Option<String>, nonce: Option<String>,
url: &str, url: &str,
) -> Result<Response, Error> { ) -> Result<Response<Body>, Error> {
self.client.post(payload, nonce, self, url).await self.client.post(payload, nonce, self, url).await
} }
} }
@ -240,17 +241,18 @@ impl Signer for AccountInner {
#[derive(Debug)] #[derive(Debug)]
struct Client { struct Client {
client: reqwest::Client, client: hyper::Client<hyper_rustls::HttpsConnector<HttpConnector>>,
urls: DirectoryUrls, urls: DirectoryUrls,
} }
impl Client { impl Client {
async fn new(server_url: &str) -> Result<Self, Error> { async fn new(server_url: &str) -> Result<Self, Error> {
let client = client()?; let client = client();
let urls = client.get(server_url).send().await?; let rsp = client.get(server_url.parse()?).await?;
let body = hyper::body::to_bytes(rsp.into_body()).await?;
Ok(Client { Ok(Client {
client, client,
urls: urls.json().await?, urls: serde_json::from_slice(&body)?,
}) })
} }
@ -260,24 +262,31 @@ impl Client {
nonce: Option<String>, nonce: Option<String>,
signer: &impl Signer, signer: &impl Signer,
url: &str, url: &str,
) -> Result<Response, Error> { ) -> Result<Response<Body>, Error> {
let nonce = match nonce { let nonce = match nonce {
Some(nonce) => nonce, Some(nonce) => nonce,
None => self.nonce().await?, None => self.nonce().await?,
}; };
Ok(self let request = Request::builder()
.client .method(Method::POST)
.post(url) .uri(url)
.header(CONTENT_TYPE, JOSE_JSON) .header(CONTENT_TYPE, JOSE_JSON)
.body(signer.signed_json(payload, &nonce, url)?) .body(signer.signed_json(payload, &nonce, url)?)
.send() .unwrap();
.await?)
Ok(self.client.request(request).await?)
} }
async fn nonce(&self) -> Result<String, Error> { async fn nonce(&self) -> Result<String, Error> {
let future = self.client.head(&self.urls.new_nonce).send(); let request = Request::builder()
match nonce_from_response(&future.await?) { .method(Method::HEAD)
.uri(&self.urls.new_nonce)
.body(Body::empty())
.unwrap();
let rsp = self.client.request(request).await?;
match nonce_from_response(&rsp) {
Some(nonce) => Ok(nonce), Some(nonce) => Ok(nonce),
None => Err("no nonce found".into()), None => Err("no nonce found".into()),
} }
@ -372,7 +381,7 @@ trait Signer {
fn key(&self) -> &Key; fn key(&self) -> &Key;
} }
fn nonce_from_response(rsp: &Response) -> Option<String> { fn nonce_from_response(rsp: &Response<Body>) -> Option<String> {
rsp.headers() rsp.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())
@ -385,8 +394,15 @@ fn base64(data: &impl Serialize) -> Result<String, serde_json::Error> {
)) ))
} }
fn client() -> Result<reqwest::Client, reqwest::Error> { fn client() -> hyper::Client<hyper_rustls::HttpsConnector<HttpConnector>> {
reqwest::Client::builder().redirect(Policy::none()).build() let https = hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()
.https_only()
.enable_http1()
.enable_http2()
.build();
hyper::Client::builder().build(https)
} }
const JOSE_JSON: &str = "application/jose+json"; const JOSE_JSON: &str = "application/jose+json";

View File

@ -1,7 +1,7 @@
use std::fmt; use std::fmt;
use base64::URL_SAFE_NO_PAD; use base64::URL_SAFE_NO_PAD;
use reqwest::Response; use hyper::{Body, Response};
use ring::digest::{digest, Digest, SHA256}; use ring::digest::{digest, Digest, SHA256};
use ring::signature::{EcdsaKeyPair, KeyPair}; use ring::signature::{EcdsaKeyPair, KeyPair};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
@ -19,7 +19,9 @@ pub enum Error {
#[error("invalid key bytes: {0}")] #[error("invalid key bytes: {0}")]
CryptoKey(#[from] ring::error::KeyRejected), CryptoKey(#[from] ring::error::KeyRejected),
#[error("HTTP request failure: {0}")] #[error("HTTP request failure: {0}")]
Http(#[from] reqwest::Error), Http(#[from] hyper::Error),
#[error("invalid URI: {0}")]
InvalidUri(#[from] hyper::http::uri::InvalidUri),
#[error("failed to (de)serialize JSON: {0}")] #[error("failed to (de)serialize JSON: {0}")]
Json(#[from] serde_json::Error), Json(#[from] serde_json::Error),
#[error("missing data: {0}")] #[error("missing data: {0}")]
@ -48,16 +50,21 @@ pub struct Problem {
} }
impl Problem { impl Problem {
pub(crate) async fn check<T: DeserializeOwned>(rsp: Response) -> Result<T, Error> { pub(crate) async fn check<T: DeserializeOwned>(rsp: Response<Body>) -> Result<T, Error> {
Ok(Self::from_response(rsp).await?.json().await?) Ok(serde_json::from_slice(
&hyper::body::to_bytes(Self::from_response(rsp).await?).await?,
)?)
} }
pub(crate) async fn from_response(rsp: Response) -> Result<Response, Error> { pub(crate) async fn from_response(rsp: Response<Body>) -> Result<Body, Error> {
let status = rsp.status(); let status = rsp.status();
match status.is_client_error() || status.is_server_error() { let body = rsp.into_body();
false => Ok(rsp), if status.is_informational() || status.is_success() || status.is_redirection() {
true => Err(rsp.json::<Problem>().await?.into()), return Ok(body);
} }
let body = hyper::body::to_bytes(body).await?;
Err(serde_json::from_slice::<Problem>(&body)?.into())
} }
} }