diff --git a/debian/control b/debian/control index 12191dec..3752fc56 100644 --- a/debian/control +++ b/debian/control @@ -113,6 +113,7 @@ Architecture: any Depends: fonts-font-awesome, libjs-extjs (>= 6.0.1), libjs-qrcodejs (>= 1.20201119), + libproxmox-acme-plugins, libsgutils2-2, libzstd1 (>= 1.3.8), lvm2, diff --git a/debian/control.in b/debian/control.in index 51386068..728e178c 100644 --- a/debian/control.in +++ b/debian/control.in @@ -3,6 +3,7 @@ Architecture: any Depends: fonts-font-awesome, libjs-extjs (>= 6.0.1), libjs-qrcodejs (>= 1.20201119), + libproxmox-acme-plugins, libsgutils2-2, libzstd1 (>= 1.3.8), lvm2, diff --git a/src/api2/config/acme.rs b/src/api2/config/acme.rs index 22a29293..7c337857 100644 --- a/src/api2/config/acme.rs +++ b/src/api2/config/acme.rs @@ -1,6 +1,10 @@ +use std::fs; use std::path::Path; +use std::sync::{Arc, Mutex}; +use std::time::SystemTime; use anyhow::{bail, format_err, Error}; +use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; @@ -386,6 +390,43 @@ fn get_directories() -> Result<&'static [KnownAcmeDirectory], Error> { Ok(crate::config::acme::KNOWN_ACME_DIRECTORIES) } +/// Wrapper for efficient Arc use when returning the ACME challenge-plugin schema for serializing +struct ChallengeSchemaWrapper { + inner: Arc>, +} + +impl Serialize for ChallengeSchemaWrapper { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.inner.serialize(serializer) + } +} + +fn get_cached_challenge_schemas() -> Result { + lazy_static! { + static ref CACHE: Mutex>, SystemTime)>> = + Mutex::new(None); + } + + // the actual loading code + let mut last = CACHE.lock().unwrap(); + + let actual_mtime = fs::metadata(crate::config::acme::ACME_DNS_SCHEMA_FN)?.modified()?; + + let schema = match &*last { + Some((schema, cached_mtime)) if *cached_mtime >= actual_mtime => schema.clone(), + _ => { + let new_schema = Arc::new(crate::config::acme::load_dns_challenge_schema()?); + *last = Some((Arc::clone(&new_schema), actual_mtime)); + new_schema + } + }; + + Ok(ChallengeSchemaWrapper { inner: schema }) +} + #[api( access: { permission: &Permission::Anybody, @@ -397,18 +438,8 @@ fn get_directories() -> Result<&'static [KnownAcmeDirectory], Error> { }, )] /// Get named known ACME directory endpoints. -fn get_challenge_schema() -> Result, Error> { - let mut out = Vec::new(); - crate::config::acme::foreach_dns_plugin(|id| { - out.push(AcmeChallengeSchema { - id: id.to_owned(), - name: id.to_owned(), - ty: "dns", - schema: Value::Object(Default::default()), - }); - ControlFlow::Continue(()) - })?; - Ok(out) +fn get_challenge_schema() -> Result { + get_cached_challenge_schemas() } #[api] diff --git a/src/config/acme/mod.rs b/src/config/acme/mod.rs index 9350551d..4fca9b10 100644 --- a/src/config/acme/mod.rs +++ b/src/config/acme/mod.rs @@ -2,12 +2,14 @@ use std::collections::HashMap; use std::path::Path; use anyhow::{bail, format_err, Error}; +use serde_json::Value; use proxmox::sys::error::SysError; -use proxmox::tools::fs::CreateOptions; +use proxmox::tools::fs::{CreateOptions, file_read_string}; use crate::api2::types::{ PROXMOX_SAFE_ID_REGEX, + AcmeChallengeSchema, KnownAcmeDirectory, AcmeAccountName, }; @@ -16,6 +18,8 @@ use crate::tools::ControlFlow; pub(crate) const ACME_DIR: &str = configdir!("/acme"); pub(crate) const ACME_ACCOUNT_DIR: &str = configdir!("/acme/accounts"); +pub(crate) const ACME_DNS_SCHEMA_FN: &str = "/usr/share/proxmox-acme/dns-challenge-schema.json"; + pub mod plugin; // `const fn`ify this once it is supported in `proxmox` @@ -141,6 +145,25 @@ pub fn mark_account_deactivated(name: &str) -> Result<(), Error> { ); } +pub fn load_dns_challenge_schema() -> Result, Error> { + let raw = file_read_string(&ACME_DNS_SCHEMA_FN)?; + let schemas: serde_json::Map = serde_json::from_str(&raw)?; + + Ok(schemas + .iter() + .map(|(id, schema)| AcmeChallengeSchema { + id: id.to_owned(), + name: schema + .get("name") + .and_then(Value::as_str) + .unwrap_or(id) + .to_owned(), + ty: "dns", + schema: schema.to_owned(), + }) + .collect()) +} + pub fn complete_acme_account(_arg: &str, _param: &HashMap) -> Vec { let mut out = Vec::new(); let _ = foreach_acme_account(|name| {