use crate::protocol::{AcmeError, Request, Response, Url};
use crate::schema;
use crate::schema::{
authorizations::Authorization as AuthorizationSchema,
orders::{CertificateChain, FinalizeOrder, NewOrderRequest},
orders::{Order as OrderSchema, OrderStatus},
Identifier,
};
use chrono::{DateTime, Utc};
use signature::Keypair;
use x509_cert::spki::DynSignatureAlgorithmIdentifier;
use super::{account::Account, authorization::Authorization, client::Client};
const CONTENT_PEM_CHAIN: &str = "application/pem-certificate-chain";
#[derive(Debug)]
pub struct Order<'a, K> {
account: &'a Account<K>,
data: schema::Order,
url: Url,
}
impl<'a, K> Order<'a, K> {
pub(crate) fn new(account: &'a Account<K>, data: schema::Order, url: Url) -> Self {
Self { account, data, url }
}
#[inline]
pub(crate) fn client(&self) -> &Client {
self.account.client()
}
#[inline]
pub(crate) fn account(&self) -> &Account<K> {
self.account
}
pub fn url(&self) -> &Url {
&self.url
}
pub fn data(&self) -> &schema::Order {
&self.data
}
pub fn status(&self) -> OrderStatus {
*self.data.status()
}
pub async fn refresh(&mut self) -> Result<(), AcmeError>
where
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
let response: Response<schema::Order> = self
.client()
.execute(Request::get(
self.url().clone(),
self.account().request_key(),
))
.await?;
self.data = response.into_inner();
Ok(())
}
pub async fn authorizations(&self) -> Result<Vec<Authorization<'_, K>>, AcmeError>
where
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
let client = self.client();
let mut authorizations = Vec::new();
for auth_url in self.data.authorizations() {
let authz: Response<AuthorizationSchema> = client
.execute(Request::get(auth_url.clone(), self.account().request_key()))
.await?;
authorizations.push(Authorization::new(
self,
authz.into_inner(),
auth_url.clone(),
));
}
Ok(authorizations)
}
pub async fn authorization(
&self,
id: &Identifier,
) -> Result<Option<Authorization<'_, K>>, AcmeError>
where
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
Ok(self
.authorizations()
.await?
.into_iter()
.find(|authn| &authn.data().identifier == id))
}
pub async fn finalize<K2, S>(&mut self, key: &K2) -> Result<(), AcmeError>
where
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
K2: signature::Signer<S> + Keypair + DynSignatureAlgorithmIdentifier,
K2::VerifyingKey:
x509_cert::spki::EncodePublicKey + crate::cert::DynSubjectPublicKeyInfoOwned,
S: x509_cert::spki::SignatureBitStringEncoding,
{
tracing::trace!("Creating CSR for finalization request");
let body = FinalizeOrder::new(&self.data, key);
let request = Request::post(
body,
self.data.finalize().clone(),
self.account().request_key(),
);
tracing::trace!("Sending order finalize request");
let info: Response<crate::schema::Order> = self.client().execute(request).await?;
self.data = info.into_inner();
Ok(())
}
async fn poll_for_order_ready(&mut self) -> Result<(), AcmeError>
where
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
self.refresh().await?;
match self.status() {
OrderStatus::Valid | OrderStatus::Invalid => {
tracing::debug!(status=?self.status(), "Order was already finished");
return Ok(());
}
OrderStatus::Ready | OrderStatus::Pending => {
return Err(AcmeError::NotReady("Order is not finalized"));
}
OrderStatus::Processing => {
tracing::trace!(status=?self.status(), "Polling for readiness");
}
}
loop {
tracing::trace!("Fetching order info to check status");
let info: Response<OrderSchema> = self
.client()
.execute(Request::get(
self.url().clone(),
self.account().request_key(),
))
.await?;
let delay = info
.retry_after()
.unwrap_or_else(|| std::time::Duration::from_secs(1));
self.data = info.into_inner();
if matches!(self.status(), OrderStatus::Valid | OrderStatus::Invalid) {
tracing::debug!(status=?self.status(), "Order is finished");
break;
}
tracing::trace!(status=?self.status(), delay=?delay, "Order is not finished");
tokio::time::sleep(delay).await;
}
Ok(())
}
pub async fn download(&self) -> Result<CertificateChain, AcmeError>
where
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
let order_info = &self.data;
let Some(url) = order_info.certificate() else {
return Err(AcmeError::NotReady("certificate"));
};
let mut request = Request::get(url.clone(), self.account().request_key());
request
.headers_mut()
.insert(reqwest::header::ACCEPT, CONTENT_PEM_CHAIN.parse().unwrap());
let certificate: Response<CertificateChain> = self.client().execute(request).await?;
Ok(certificate.into_inner())
}
pub async fn finalize_and_download<K2, S>(
&mut self,
key: &K2,
) -> Result<CertificateChain, AcmeError>
where
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
K2: signature::Signer<S> + Keypair + DynSignatureAlgorithmIdentifier,
K2::VerifyingKey:
x509_cert::spki::EncodePublicKey + crate::cert::DynSubjectPublicKeyInfoOwned,
S: x509_cert::spki::SignatureBitStringEncoding,
{
self.finalize(key).await?;
self.poll_for_order_ready().await?;
self.download().await
}
}
#[derive(Debug)]
pub struct OrderBuilder<'a, K> {
account: &'a Account<K>,
identifiers: Vec<Identifier>,
#[cfg(feature = "acme-profiles")]
profile: Option<String>,
not_before: Option<DateTime<Utc>>,
not_after: Option<DateTime<Utc>>,
}
impl<'a, K> OrderBuilder<'a, K> {
pub(crate) fn new(account: &'a Account<K>) -> Self {
Self {
account,
identifiers: Vec::new(),
#[cfg(feature = "acme-profiles")]
profile: None,
not_before: None,
not_after: None,
}
}
pub fn push(mut self, identifier: Identifier) -> Self {
self.identifiers.push(identifier);
self
}
pub fn dns<S: Into<String>>(mut self, identifier: S) -> Self {
self.identifiers.push(Identifier::dns(identifier.into()));
self
}
#[cfg(feature = "acme-profiles")]
pub fn profile<S: Into<String>>(mut self, profile: S) -> Self {
self.profile = Some(profile.into());
self
}
pub fn start(mut self, when: DateTime<Utc>) -> Self {
self.not_before = Some(when);
self
}
pub fn end(mut self, when: DateTime<Utc>) -> Self {
self.not_after = Some(when);
self
}
pub async fn create(self) -> Result<Order<'a, K>, AcmeError>
where
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
let account = self.account;
let payload = NewOrderRequest {
identifiers: self.identifiers,
#[cfg(feature = "acme-profiles")]
profile: self.profile,
not_before: self.not_before,
not_after: self.not_after,
};
let order: Response<crate::schema::Order> = account
.client()
.execute(Request::post(
payload,
account.directory().new_order.clone(),
account.request_key(),
))
.await?;
let order_url = order.location().expect("New order should have a location");
let order = Order::new(account, order.into_inner(), order_url);
Ok(order)
}
pub async fn get(self, url: Url) -> Result<Order<'a, K>, AcmeError>
where
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
let order: Response<crate::schema::Order> = self
.account
.client()
.execute(Request::get(url.clone(), self.account.request_key()))
.await?;
let order = Order::new(self.account, order.into_inner(), url);
Ok(order)
}
}
pub(crate) async fn list<K>(
account: &Account<K>,
limit: Option<usize>,
) -> Result<Vec<Order<'_, K>>, AcmeError>
where
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
let client = account.client();
let mut request = Request::get(
account
.data()
.orders
.as_ref()
.ok_or_else(|| AcmeError::MissingData("orders url"))?
.clone(),
account.request_key(),
);
let mut orders = Vec::new();
let mut page = 0;
loop {
tracing::debug!("Fetching orders, page {page}");
let response = client.execute(request.clone()).await?;
let orders_page: schema::orders::Orders = response.into_inner();
for order_url in orders_page.orders {
let response: Response<OrderSchema> = client
.execute(Request::get(order_url.clone(), account.request_key()))
.await?;
let order = Order::new(account, response.into_inner(), order_url);
orders.push(order);
}
if let Some(lim) = limit {
if orders.len() >= lim {
return Ok(orders);
}
}
match orders_page.next {
Some(next_url) => {
request = request.with_url(next_url);
}
None => return Ok(orders),
}
page += 1;
}
}