use reqwest::header::HeaderMap;
use reqwest::Certificate;
use serde::{Deserialize, Serialize};
use crate::protocol::errors::AcmeHTTPError;
use super::errors::{AcmeError, AcmeErrorCode, AcmeErrorDocument};
use super::jose::Nonce;
use super::request::Key;
use super::response::{Decode, Response};
use super::Request;
use super::Url;
pub use AcmeClient as Client;
#[cfg(feature = "trace-requests")]
use jaws::fmt::JWTFormat;
#[cfg(feature = "trace-requests")]
use super::request::Encode;
const NONCE_HEADER: &str = "Replay-Nonce";
#[derive(Debug)]
pub struct ClientBuilder {
inner: reqwest::ClientBuilder,
new_nonce: Option<Url>,
configuration: AcmeClientConfiguration,
}
impl Default for ClientBuilder {
fn default() -> Self {
Self::new()
}
}
impl ClientBuilder {
pub(crate) fn new() -> Self {
let builder =
reqwest::Client::builder().user_agent(concat!("YACME / ", env!("CARGO_PKG_VERSION")));
ClientBuilder {
inner: builder,
new_nonce: None,
configuration: Default::default(),
}
}
pub fn with_nonce_url(mut self, url: Url) -> Self {
self.new_nonce = Some(url);
self
}
pub fn add_root_certificate(mut self, cert: Certificate) -> Self {
self.inner = self.inner.add_root_certificate(cert);
self
}
pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
self.inner = self.inner.timeout(timeout);
self
}
pub fn connect_timeout(mut self, timeout: std::time::Duration) -> Self {
self.inner = self.inner.connect_timeout(timeout);
self
}
pub fn bad_nonce_retries(mut self, retries: usize) -> Self {
self.configuration.nonce_retries = retries;
self
}
pub fn build(self) -> Result<AcmeClient, reqwest::Error> {
Ok(AcmeClient {
inner: self.inner.build()?,
nonce: None,
new_nonce: self.new_nonce,
configuration: self.configuration,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AcmeClientConfiguration {
pub nonce_retries: usize,
}
impl Default for AcmeClientConfiguration {
fn default() -> Self {
Self { nonce_retries: 3 }
}
}
#[derive(Debug, Default)]
pub struct AcmeClient {
pub(super) inner: reqwest::Client,
nonce: Option<Nonce>,
new_nonce: Option<Url>,
configuration: AcmeClientConfiguration,
}
impl AcmeClient {
pub fn builder() -> ClientBuilder {
ClientBuilder::new()
}
pub fn set_new_nonce_url(&mut self, url: Url) {
self.new_nonce = Some(url);
}
}
impl AcmeClient {
pub async fn get<R>(&mut self, url: Url) -> Result<Response<R>, AcmeError>
where
R: Decode,
{
let response = self.inner.get(url.as_str()).send().await?;
Response::from_decoded_response(response).await
}
#[cfg(all(feature = "trace-requests", not(doc)))]
pub async fn post<P, K, L, R>(
&mut self,
url: Url,
payload: P,
key: K,
) -> Result<Response<R>, AcmeError>
where
P: Serialize,
R: Decode + Encode,
K: Into<Key<L>>,
L: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
let request = Request::post(payload, url, key);
self.execute(request).await
}
#[cfg(all(feature = "trace-requests", not(doc)))]
pub async fn get_as_post<K, L, R>(&mut self, url: Url, key: K) -> Result<Response<R>, AcmeError>
where
R: Decode + Encode,
K: Into<Key<L>>,
L: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
let request = Request::get(url, key);
self.execute(request).await
}
#[cfg(any(not(feature = "trace-requests"), doc))]
pub async fn post<P, K, L, R>(
&mut self,
url: Url,
payload: P,
key: K,
) -> Result<Response<R>, AcmeError>
where
P: Serialize,
R: Decode,
K: Into<Key<L>>,
L: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
let request = Request::post(payload, url, key);
self.execute(request).await
}
#[cfg(any(not(feature = "trace-requests"), doc))]
pub async fn get_as_post<K, L, R>(&mut self, url: Url, key: K) -> Result<Response<R>, AcmeError>
where
R: Decode,
K: Into<Key<L>>,
L: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
let request = Request::get(url, key);
self.execute(request).await
}
#[cfg(any(not(feature = "trace-requests"), doc))]
pub async fn execute<P, K, R>(
&mut self,
request: Request<P, K>,
) -> Result<Response<R>, AcmeError>
where
P: Serialize,
R: Decode,
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
Response::from_decoded_response(self.execute_internal(request).await?).await
}
#[cfg(all(feature = "trace-requests", not(doc)))]
pub async fn execute<P, K, R>(
&mut self,
request: Request<P, K>,
) -> Result<Response<R>, AcmeError>
where
P: Serialize,
R: Decode + Encode,
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
tracing::trace!("REQ: \n{}", request.as_signed().formatted());
Response::from_decoded_response(self.execute_internal(request).await?)
.await
.inspect(|r| {
tracing::trace!("RES: \n{}", r.formatted());
})
}
#[inline]
async fn execute_internal<P, K>(
&mut self,
request: Request<P, K>,
) -> Result<reqwest::Response, AcmeError>
where
P: Serialize,
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
let mut nonce = self.get_nonce().await?;
for retry in 0..self.configuration.nonce_retries {
let signed = request.sign(nonce)?;
let response = self.inner.execute(signed.into_inner()).await?;
self.record_nonce(response.headers())?;
if response.status().is_success() {
return Ok(response);
}
match process_error_response(response).await {
AcmeError::Acme(document) if matches!(document.kind(), AcmeErrorCode::BadNonce) => {
tracing::trace!(%retry, "Retrying request with next nonce");
nonce = self.get_nonce().await?;
}
error => {
return Err(error);
}
}
}
tracing::trace!("Nonce retries exhausted");
Err(AcmeError::NonceRetriesExhausted(
self.configuration.nonce_retries,
))
}
}
async fn process_error_response(response: reqwest::Response) -> AcmeError {
debug_assert!(
!response.status().is_success(),
"expected to process an error result"
);
let status = response.status();
let body = match response.bytes().await {
Ok(body) => body,
Err(error) => {
return AcmeError::HttpRequest(error);
}
};
if body.is_empty() {
return AcmeHTTPError::new(status, None).into();
}
let document: AcmeErrorDocument = match serde_json::from_slice(&body) {
Ok(error) => error,
Err(error) if status.is_client_error() => {
tracing::error!(%status, "Failed to parse error document {}: {}", error, String::from_utf8_lossy(&body));
return AcmeHTTPError::new(status, Some(body)).into();
}
Err(_) => {
let text = String::from_utf8_lossy(&body);
tracing::trace!(%status, "RES: \n{}", text);
return AcmeHTTPError::new(status, Some(body)).into();
}
};
let text = String::from_utf8_lossy(&body);
tracing::trace!(%document, "RES: \n{}", text);
AcmeError::Acme(document)
}
pub(crate) fn extract_nonce(headers: &HeaderMap) -> Result<Nonce, AcmeError> {
let value = headers.get(NONCE_HEADER).ok_or(AcmeError::MissingNonce)?;
Ok(Nonce::from(
value
.to_str()
.map_err(|_| AcmeError::InvalidNonce(Some(value.clone())))?
.to_owned(),
))
}
impl AcmeClient {
fn record_nonce(&mut self, headers: &HeaderMap) -> Result<(), AcmeError> {
self.nonce = Some(extract_nonce(headers)?);
Ok(())
}
async fn get_nonce(&mut self) -> Result<Nonce, AcmeError> {
if let Some(value) = self.nonce.take() {
return Ok(value);
}
if let Some(url) = &self.new_nonce {
tracing::debug!("Requesting a new nonce");
let response = self
.inner
.head(url.as_str())
.send()
.await
.map_err(AcmeError::nonce)?;
response.error_for_status_ref().map_err(AcmeError::nonce)?;
let value = extract_nonce(response.headers())?;
Ok(value)
} else {
tracing::warn!("No nonce URL provided, unable to fetch new nonce");
Err(AcmeError::MissingNonce)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_nonce_from_header() {
let response = crate::response!("new-nonce.http");
let nonce = extract_nonce(response.headers()).unwrap();
assert_eq!(nonce.as_ref(), "oFvnlFP1wIhRlYS2jTaXbA");
}
}