use serde::Deserialize;
use crate::error::{AcmeError, Error, Result};
#[derive(Debug, Clone, Deserialize, Default)]
pub struct Directory {
#[serde(rename = "newNonce")]
pub new_nonce: Option<String>,
#[serde(rename = "newAccount")]
pub new_account: Option<String>,
#[serde(rename = "newOrder")]
pub new_order: Option<String>,
#[serde(rename = "newAuthz")]
pub new_authz: Option<String>,
#[serde(rename = "revokeCert")]
pub revoke_cert: Option<String>,
#[serde(rename = "keyChange")]
pub key_change: Option<String>,
#[serde(rename = "renewalInfo")]
pub renewal_info: Option<String>,
#[serde(default)]
pub meta: Option<DirectoryMeta>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct DirectoryMeta {
#[serde(rename = "termsOfService")]
pub terms_of_service: Option<String>,
#[serde(default)]
pub website: Option<String>,
#[serde(default)]
pub caa_identities: Vec<String>,
#[serde(rename = "externalAccountRequired", default)]
pub external_account_required: bool,
#[serde(default, deserialize_with = "deserialize_profiles")]
pub profiles: ProfileList,
}
#[derive(Debug, Clone, Default)]
pub struct ProfileList(pub Vec<String>);
fn deserialize_profiles<'de, D>(deserializer: D) -> Result<ProfileList, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
let Some(value) = value else {
return Ok(ProfileList::default());
};
match value {
serde_json::Value::Array(items) => Ok(ProfileList(
items
.into_iter()
.filter_map(|v| v.as_str().map(str::to_owned))
.collect(),
)),
serde_json::Value::Object(map) => Ok(ProfileList(map.keys().cloned().collect())),
_ => Ok(ProfileList::default()),
}
}
impl Directory {
pub async fn fetch(
transport: &dyn super::transport::Transport,
directory_url: &str,
) -> Result<Self> {
let resp = transport
.execute(super::transport::HttpRequest {
method: super::transport::Method::Get,
url: directory_url.to_owned(),
body: None,
content_type: None,
accept: Some("application/json".into()),
})
.await?;
if !resp.is_success() {
return Err(Error::Acme(AcmeError::Directory(format!(
"directory fetch returned HTTP {}",
resp.status
))));
}
serde_json::from_slice(&resp.body)
.map_err(|e| Error::Acme(AcmeError::Directory(format!("invalid JSON: {e}"))))
}
pub fn new_account(&self) -> Result<&str> {
self.new_account
.as_deref()
.ok_or_else(missing("newAccount"))
}
pub fn new_order(&self) -> Result<&str> {
self.new_order.as_deref().ok_or_else(missing("newOrder"))
}
pub fn new_nonce(&self) -> Result<&str> {
self.new_nonce.as_deref().ok_or_else(missing("newNonce"))
}
pub fn revoke_cert(&self) -> Result<&str> {
self.revoke_cert
.as_deref()
.ok_or_else(missing("revokeCert"))
}
pub fn renewal_info(&self) -> Result<&str> {
self.renewal_info
.as_deref()
.ok_or_else(missing("renewalInfo"))
}
}
fn missing(name: &str) -> impl Fn() -> Error + '_ {
move || Error::Acme(AcmeError::MissingEndpoint(name.to_owned()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::acme::transport::{HttpRequest, HttpResponse};
use async_trait::async_trait;
use std::sync::Arc;
#[derive(Debug)]
struct StaticTransport(HttpResponse);
#[async_trait]
impl crate::acme::transport::Transport for StaticTransport {
async fn execute(&self, _req: HttpRequest) -> Result<HttpResponse> {
Ok(self.0.clone())
}
}
#[tokio::test]
async fn parses_directory_json() {
let body = br#"{
"newNonce": "https://ca/nnonce",
"newAccount": "https://ca/nacct",
"newOrder": "https://ca/norder",
"revokeCert": "https://ca/revoke",
"keyChange": "https://ca/kc",
"renewalInfo": "https://ca/ri",
"meta": {"termsOfService": "https://ca/tos", "externalAccountRequired": true}
}"#;
let transport = StaticTransport(HttpResponse {
status: 200,
headers: Default::default(),
body: body.to_vec(),
});
let dir = Directory::fetch(&transport, "https://ca/directory")
.await
.unwrap();
assert_eq!(dir.new_order().unwrap(), "https://ca/norder");
assert_eq!(dir.renewal_info().unwrap(), "https://ca/ri");
assert!(dir.meta.as_ref().unwrap().external_account_required);
assert_eq!(
dir.meta.as_ref().unwrap().terms_of_service.as_deref(),
Some("https://ca/tos")
);
let _ = Arc::new(());
}
#[tokio::test]
async fn missing_endpoint_is_reported() {
let dir = Directory::default();
assert!(matches!(
dir.new_order(),
Err(Error::Acme(AcmeError::MissingEndpoint(_)))
));
}
}