From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 444871FF17C for ; Tue, 2 Dec 2025 16:57:28 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id EE03516C6A; Tue, 2 Dec 2025 16:57:46 +0100 (CET) From: Samuel Rufinatscha To: pbs-devel@lists.proxmox.com Date: Tue, 2 Dec 2025 16:56:55 +0100 Message-ID: <20251202155659.379848-5-s.rufinatscha@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20251202155659.379848-1-s.rufinatscha@proxmox.com> References: <20251202155659.379848-1-s.rufinatscha@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1764690986552 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.306 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pbs-devel] [PATCH proxmox-backup 4/4] acme: certificate ordering through proxmox-acme-api X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Backup Server development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pbs-devel-bounces@lists.proxmox.com Sender: "pbs-devel" PBS currently uses its own ACME client and API logic, while PDM uses the factored out proxmox-acme and proxmox-acme-api crates. This duplication risks differences in behaviour and requires ACME maintenance in two places. This patch is part of a series to move PBS over to the shared ACME stack. Changes: - Replace the custom ACME order/authorization loop in node certificates with a call to proxmox_acme_api::order_certificate. - Build domain + config data as proxmox-acme-api types - Remove obsolete local ACME ordering and plugin glue code. Signed-off-by: Samuel Rufinatscha --- src/acme/mod.rs | 2 - src/acme/plugin.rs | 336 ---------------------------------- src/api2/node/certificates.rs | 240 ++++-------------------- src/api2/types/acme.rs | 74 -------- src/api2/types/mod.rs | 3 - src/config/acme/mod.rs | 7 +- src/config/acme/plugin.rs | 99 +--------- src/config/node.rs | 22 +-- src/lib.rs | 2 - 9 files changed, 46 insertions(+), 739 deletions(-) delete mode 100644 src/acme/mod.rs delete mode 100644 src/acme/plugin.rs delete mode 100644 src/api2/types/acme.rs diff --git a/src/acme/mod.rs b/src/acme/mod.rs deleted file mode 100644 index cc561f9a..00000000 --- a/src/acme/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) mod plugin; -pub(crate) use plugin::get_acme_plugin; diff --git a/src/acme/plugin.rs b/src/acme/plugin.rs deleted file mode 100644 index 5bc09e1f..00000000 --- a/src/acme/plugin.rs +++ /dev/null @@ -1,336 +0,0 @@ -use std::future::Future; -use std::net::{IpAddr, SocketAddr}; -use std::pin::Pin; -use std::process::Stdio; -use std::sync::Arc; -use std::time::Duration; - -use anyhow::{bail, format_err, Error}; -use bytes::Bytes; -use futures::TryFutureExt; -use http_body_util::Full; -use hyper::body::Incoming; -use hyper::server::conn::http1; -use hyper::service::service_fn; -use hyper::{Request, Response}; -use hyper_util::rt::TokioIo; -use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWriteExt, BufReader}; -use tokio::net::TcpListener; -use tokio::process::Command; - -use proxmox_acme::{Authorization, Challenge}; - -use crate::api2::types::AcmeDomain; -use proxmox_acme::async_client::AcmeClient; -use proxmox_rest_server::WorkerTask; - -use crate::config::acme::plugin::{DnsPlugin, PluginData}; - -const PROXMOX_ACME_SH_PATH: &str = "/usr/share/proxmox-acme/proxmox-acme"; - -pub(crate) fn get_acme_plugin( - plugin_data: &PluginData, - name: &str, -) -> Result>, Error> { - let (ty, data) = match plugin_data.get(name) { - Some(plugin) => plugin, - None => return Ok(None), - }; - - Ok(Some(match ty.as_str() { - "dns" => { - let plugin: DnsPlugin = serde::Deserialize::deserialize(data)?; - Box::new(plugin) - } - "standalone" => { - // this one has no config - Box::::default() - } - other => bail!("missing implementation for plugin type '{}'", other), - })) -} - -pub(crate) trait AcmePlugin { - /// Setup everything required to trigger the validation and return the corresponding validation - /// URL. - fn setup<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>( - &'a mut self, - client: &'b mut AcmeClient, - authorization: &'c Authorization, - domain: &'d AcmeDomain, - task: Arc, - ) -> Pin> + Send + 'fut>>; - - fn teardown<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>( - &'a mut self, - client: &'b mut AcmeClient, - authorization: &'c Authorization, - domain: &'d AcmeDomain, - task: Arc, - ) -> Pin> + Send + 'fut>>; -} - -fn extract_challenge<'a>( - authorization: &'a Authorization, - ty: &str, -) -> Result<&'a Challenge, Error> { - authorization - .challenges - .iter() - .find(|ch| ch.ty == ty) - .ok_or_else(|| format_err!("no supported challenge type ({}) found", ty)) -} - -async fn pipe_to_tasklog( - pipe: T, - task: Arc, -) -> Result<(), std::io::Error> { - let mut pipe = BufReader::new(pipe); - let mut line = String::new(); - loop { - line.clear(); - match pipe.read_line(&mut line).await { - Ok(0) => return Ok(()), - Ok(_) => task.log_message(line.as_str()), - Err(err) => return Err(err), - } - } -} - -impl DnsPlugin { - async fn action<'a>( - &self, - client: &mut AcmeClient, - authorization: &'a Authorization, - domain: &AcmeDomain, - task: Arc, - action: &str, - ) -> Result<&'a str, Error> { - let challenge = extract_challenge(authorization, "dns-01")?; - let mut stdin_data = client - .dns_01_txt_value( - challenge - .token() - .ok_or_else(|| format_err!("missing token in challenge"))?, - )? - .into_bytes(); - stdin_data.push(b'\n'); - stdin_data.extend(self.data.as_bytes()); - if stdin_data.last() != Some(&b'\n') { - stdin_data.push(b'\n'); - } - - let mut command = Command::new("/usr/bin/setpriv"); - - #[rustfmt::skip] - command.args([ - "--reuid", "nobody", - "--regid", "nogroup", - "--clear-groups", - "--reset-env", - "--", - "/bin/bash", - PROXMOX_ACME_SH_PATH, - action, - &self.core.api, - domain.alias.as_deref().unwrap_or(&domain.domain), - ]); - - // We could use 1 socketpair, but tokio wraps them all in `File` internally causing `close` - // to be called separately on all of them without exception, so we need 3 pipes :-( - - let mut child = command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; - - let mut stdin = child.stdin.take().expect("Stdio::piped()"); - let stdout = child.stdout.take().expect("Stdio::piped() failed?"); - let stdout = pipe_to_tasklog(stdout, Arc::clone(&task)); - let stderr = child.stderr.take().expect("Stdio::piped() failed?"); - let stderr = pipe_to_tasklog(stderr, Arc::clone(&task)); - let stdin = async move { - stdin.write_all(&stdin_data).await?; - stdin.flush().await?; - Ok::<_, std::io::Error>(()) - }; - match futures::try_join!(stdin, stdout, stderr) { - Ok(((), (), ())) => (), - Err(err) => { - if let Err(err) = child.kill().await { - task.log_message(format!( - "failed to kill '{PROXMOX_ACME_SH_PATH} {action}' command: {err}" - )); - } - bail!("'{}' failed: {}", PROXMOX_ACME_SH_PATH, err); - } - } - - let status = child.wait().await?; - if !status.success() { - bail!( - "'{} {}' exited with error ({})", - PROXMOX_ACME_SH_PATH, - action, - status.code().unwrap_or(-1) - ); - } - - Ok(&challenge.url) - } -} - -impl AcmePlugin for DnsPlugin { - fn setup<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>( - &'a mut self, - client: &'b mut AcmeClient, - authorization: &'c Authorization, - domain: &'d AcmeDomain, - task: Arc, - ) -> Pin> + Send + 'fut>> { - Box::pin(async move { - let result = self - .action(client, authorization, domain, task.clone(), "setup") - .await; - - let validation_delay = self.core.validation_delay.unwrap_or(30) as u64; - if validation_delay > 0 { - task.log_message(format!( - "Sleeping {validation_delay} seconds to wait for TXT record propagation" - )); - tokio::time::sleep(Duration::from_secs(validation_delay)).await; - } - result - }) - } - - fn teardown<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>( - &'a mut self, - client: &'b mut AcmeClient, - authorization: &'c Authorization, - domain: &'d AcmeDomain, - task: Arc, - ) -> Pin> + Send + 'fut>> { - Box::pin(async move { - self.action(client, authorization, domain, task, "teardown") - .await - .map(drop) - }) - } -} - -#[derive(Default)] -struct StandaloneServer { - abort_handle: Option, -} - -// In case the "order_certificates" future gets dropped between setup & teardown, let's also cancel -// the HTTP listener on Drop: -impl Drop for StandaloneServer { - fn drop(&mut self) { - self.stop(); - } -} - -impl StandaloneServer { - fn stop(&mut self) { - if let Some(abort) = self.abort_handle.take() { - abort.abort(); - } - } -} - -async fn standalone_respond( - req: Request, - path: Arc, - key_auth: Arc, -) -> Result>, hyper::Error> { - if req.method() == hyper::Method::GET && req.uri().path() == path.as_str() { - Ok(Response::builder() - .status(hyper::http::StatusCode::OK) - .body(key_auth.as_bytes().to_vec().into()) - .unwrap()) - } else { - Ok(Response::builder() - .status(hyper::http::StatusCode::NOT_FOUND) - .body("Not found.".into()) - .unwrap()) - } -} - -impl AcmePlugin for StandaloneServer { - fn setup<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>( - &'a mut self, - client: &'b mut AcmeClient, - authorization: &'c Authorization, - _domain: &'d AcmeDomain, - _task: Arc, - ) -> Pin> + Send + 'fut>> { - Box::pin(async move { - self.stop(); - - let challenge = extract_challenge(authorization, "http-01")?; - let token = challenge - .token() - .ok_or_else(|| format_err!("missing token in challenge"))?; - let key_auth = Arc::new(client.key_authorization(token)?); - let path = Arc::new(format!("/.well-known/acme-challenge/{token}")); - - // `[::]:80` first, then `*:80` - let dual = SocketAddr::new(IpAddr::from([0u16; 8]), 80); - let ipv4 = SocketAddr::new(IpAddr::from([0u8; 4]), 80); - let incoming = TcpListener::bind(dual) - .or_else(|_| TcpListener::bind(ipv4)) - .await?; - - let server = async move { - loop { - let key_auth = Arc::clone(&key_auth); - let path = Arc::clone(&path); - match incoming.accept().await { - Ok((tcp, _)) => { - let io = TokioIo::new(tcp); - let service = service_fn(move |request| { - standalone_respond( - request, - Arc::clone(&path), - Arc::clone(&key_auth), - ) - }); - - tokio::task::spawn(async move { - if let Err(err) = - http1::Builder::new().serve_connection(io, service).await - { - println!("Error serving connection: {err:?}"); - } - }); - } - Err(err) => println!("Error accepting connection: {err:?}"), - } - } - }; - let (future, abort) = futures::future::abortable(server); - self.abort_handle = Some(abort); - tokio::spawn(future); - - Ok(challenge.url.as_str()) - }) - } - - fn teardown<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>( - &'a mut self, - _client: &'b mut AcmeClient, - _authorization: &'c Authorization, - _domain: &'d AcmeDomain, - _task: Arc, - ) -> Pin> + Send + 'fut>> { - Box::pin(async move { - if let Some(abort) = self.abort_handle.take() { - abort.abort(); - } - Ok(()) - }) - } -} diff --git a/src/api2/node/certificates.rs b/src/api2/node/certificates.rs index 31196715..2a645b4a 100644 --- a/src/api2/node/certificates.rs +++ b/src/api2/node/certificates.rs @@ -1,27 +1,19 @@ -use std::sync::Arc; -use std::time::Duration; - use anyhow::{bail, format_err, Error}; use openssl::pkey::PKey; use openssl::x509::X509; use serde::{Deserialize, Serialize}; use tracing::info; -use proxmox_router::list_subdirs_api_method; -use proxmox_router::SubdirMap; -use proxmox_router::{Permission, Router, RpcEnvironment}; -use proxmox_schema::api; - +use crate::server::send_certificate_renewal_mail; use pbs_api_types::{NODE_SCHEMA, PRIV_SYS_MODIFY}; use pbs_buildcfg::configdir; use pbs_tools::cert; -use tracing::warn; - -use crate::api2::types::AcmeDomain; -use crate::config::node::NodeConfig; -use crate::server::send_certificate_renewal_mail; -use proxmox_acme::async_client::AcmeClient; +use proxmox_acme_api::AcmeDomain; use proxmox_rest_server::WorkerTask; +use proxmox_router::list_subdirs_api_method; +use proxmox_router::SubdirMap; +use proxmox_router::{Permission, Router, RpcEnvironment}; +use proxmox_schema::api; pub const ROUTER: Router = Router::new() .get(&list_subdirs_api_method!(SUBDIRS)) @@ -269,193 +261,6 @@ pub async fn delete_custom_certificate() -> Result<(), Error> { Ok(()) } -struct OrderedCertificate { - certificate: hyper::body::Bytes, - private_key_pem: Vec, -} - -async fn order_certificate( - worker: Arc, - node_config: &NodeConfig, -) -> Result, Error> { - use proxmox_acme::authorization::Status; - use proxmox_acme::order::Identifier; - - let domains = node_config.acme_domains().try_fold( - Vec::::new(), - |mut acc, domain| -> Result<_, Error> { - let mut domain = domain?; - domain.domain.make_ascii_lowercase(); - if let Some(alias) = &mut domain.alias { - alias.make_ascii_lowercase(); - } - acc.push(domain); - Ok(acc) - }, - )?; - - let get_domain_config = |domain: &str| { - domains - .iter() - .find(|d| d.domain == domain) - .ok_or_else(|| format_err!("no config for domain '{}'", domain)) - }; - - if domains.is_empty() { - info!("No domains configured to be ordered from an ACME server."); - return Ok(None); - } - - let (plugins, _) = crate::config::acme::plugin::config()?; - - let mut acme = node_config.acme_client().await?; - - info!("Placing ACME order"); - let order = acme - .new_order(domains.iter().map(|d| d.domain.to_ascii_lowercase())) - .await?; - info!("Order URL: {}", order.location); - - let identifiers: Vec = order - .data - .identifiers - .iter() - .map(|identifier| match identifier { - Identifier::Dns(domain) => domain.clone(), - }) - .collect(); - - for auth_url in &order.data.authorizations { - info!("Getting authorization details from '{auth_url}'"); - let mut auth = acme.get_authorization(auth_url).await?; - - let domain = match &mut auth.identifier { - Identifier::Dns(domain) => domain.to_ascii_lowercase(), - }; - - if auth.status == Status::Valid { - info!("{domain} is already validated!"); - continue; - } - - info!("The validation for {domain} is pending"); - let domain_config: &AcmeDomain = get_domain_config(&domain)?; - let plugin_id = domain_config.plugin.as_deref().unwrap_or("standalone"); - let mut plugin_cfg = crate::acme::get_acme_plugin(&plugins, plugin_id)? - .ok_or_else(|| format_err!("plugin '{plugin_id}' for domain '{domain}' not found!"))?; - - info!("Setting up validation plugin"); - let validation_url = plugin_cfg - .setup(&mut acme, &auth, domain_config, Arc::clone(&worker)) - .await?; - - let result = request_validation(&mut acme, auth_url, validation_url).await; - - if let Err(err) = plugin_cfg - .teardown(&mut acme, &auth, domain_config, Arc::clone(&worker)) - .await - { - warn!("Failed to teardown plugin '{plugin_id}' for domain '{domain}' - {err}"); - } - - result?; - } - - info!("All domains validated"); - info!("Creating CSR"); - - let csr = proxmox_acme::util::Csr::generate(&identifiers, &Default::default())?; - let mut finalize_error_cnt = 0u8; - let order_url = &order.location; - let mut order; - loop { - use proxmox_acme::order::Status; - - order = acme.get_order(order_url).await?; - - match order.status { - Status::Pending => { - info!("still pending, trying to finalize anyway"); - let finalize = order - .finalize - .as_deref() - .ok_or_else(|| format_err!("missing 'finalize' URL in order"))?; - if let Err(err) = acme.finalize(finalize, &csr.data).await { - if finalize_error_cnt >= 5 { - return Err(err); - } - - finalize_error_cnt += 1; - } - tokio::time::sleep(Duration::from_secs(5)).await; - } - Status::Ready => { - info!("order is ready, finalizing"); - let finalize = order - .finalize - .as_deref() - .ok_or_else(|| format_err!("missing 'finalize' URL in order"))?; - acme.finalize(finalize, &csr.data).await?; - tokio::time::sleep(Duration::from_secs(5)).await; - } - Status::Processing => { - info!("still processing, trying again in 30 seconds"); - tokio::time::sleep(Duration::from_secs(30)).await; - } - Status::Valid => { - info!("valid"); - break; - } - other => bail!("order status: {:?}", other), - } - } - - info!("Downloading certificate"); - let certificate = acme - .get_certificate( - order - .certificate - .as_deref() - .ok_or_else(|| format_err!("missing certificate url in finalized order"))?, - ) - .await?; - - Ok(Some(OrderedCertificate { - certificate, - private_key_pem: csr.private_key_pem, - })) -} - -async fn request_validation( - acme: &mut AcmeClient, - auth_url: &str, - validation_url: &str, -) -> Result<(), Error> { - info!("Triggering validation"); - acme.request_challenge_validation(validation_url).await?; - - info!("Sleeping for 5 seconds"); - tokio::time::sleep(Duration::from_secs(5)).await; - - loop { - use proxmox_acme::authorization::Status; - - let auth = acme.get_authorization(auth_url).await?; - match auth.status { - Status::Pending => { - info!("Status is still 'pending', trying again in 10 seconds"); - tokio::time::sleep(Duration::from_secs(10)).await; - } - Status::Valid => return Ok(()), - other => bail!( - "validating challenge '{}' failed - status: {:?}", - validation_url, - other - ), - } - } -} - #[api( input: { properties: { @@ -525,9 +330,30 @@ fn spawn_certificate_worker( let auth_id = rpcenv.get_auth_id().unwrap(); + let acme_config = if let Some(cfg) = node_config.acme_config().transpose()? { + cfg + } else { + proxmox_acme_api::parse_acme_config_string("account=default")? + }; + + let domains = node_config.acme_domains().try_fold( + Vec::::new(), + |mut acc, domain| -> Result<_, Error> { + let mut domain = domain?; + domain.domain.make_ascii_lowercase(); + if let Some(alias) = &mut domain.alias { + alias.make_ascii_lowercase(); + } + acc.push(domain); + Ok(acc) + }, + )?; + WorkerTask::spawn(name, None, auth_id, true, move |worker| async move { let work = || async { - if let Some(cert) = order_certificate(worker, &node_config).await? { + if let Some(cert) = + proxmox_acme_api::order_certificate(worker, &acme_config, &domains).await? + { crate::config::set_proxy_certificate(&cert.certificate, &cert.private_key_pem)?; crate::server::reload_proxy_certificate().await?; } @@ -563,16 +389,20 @@ pub fn revoke_acme_cert(rpcenv: &mut dyn RpcEnvironment) -> Result, - - /// The plugin to use to validate this domain. - /// - /// Empty means standalone HTTP validation is used. - #[serde(skip_serializing_if = "Option::is_none")] - pub plugin: Option, -} - -pub const ACME_DOMAIN_PROPERTY_SCHEMA: Schema = - StringSchema::new("ACME domain configuration string") - .format(&ApiStringFormat::PropertyString(&AcmeDomain::API_SCHEMA)) - .schema(); - -#[api( - properties: { - schema: { - type: Object, - additional_properties: true, - properties: {}, - }, - type: { - type: String, - }, - }, -)] -#[derive(Serialize)] -/// Schema for an ACME challenge plugin. -pub struct AcmeChallengeSchema { - /// Plugin ID. - pub id: String, - - /// Human readable name, falls back to id. - pub name: String, - - /// Plugin Type. - #[serde(rename = "type")] - pub ty: &'static str, - - /// The plugin's parameter schema. - pub schema: Value, -} diff --git a/src/api2/types/mod.rs b/src/api2/types/mod.rs index afc34b30..34193685 100644 --- a/src/api2/types/mod.rs +++ b/src/api2/types/mod.rs @@ -4,9 +4,6 @@ use anyhow::bail; use proxmox_schema::*; -mod acme; -pub use acme::*; - // File names: may not contain slashes, may not start with "." pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| { if name.starts_with('.') { diff --git a/src/config/acme/mod.rs b/src/config/acme/mod.rs index 35cda50b..afd7abf8 100644 --- a/src/config/acme/mod.rs +++ b/src/config/acme/mod.rs @@ -9,8 +9,7 @@ use proxmox_sys::fs::{file_read_string, CreateOptions}; use pbs_api_types::PROXMOX_SAFE_ID_REGEX; -use crate::api2::types::AcmeChallengeSchema; -use proxmox_acme_api::{AcmeAccountName, KnownAcmeDirectory, KNOWN_ACME_DIRECTORIES}; +use proxmox_acme_api::{AcmeAccountName, AcmeChallengeSchema}; pub(crate) const ACME_DIR: &str = pbs_buildcfg::configdir!("/acme"); pub(crate) const ACME_ACCOUNT_DIR: &str = pbs_buildcfg::configdir!("/acme/accounts"); @@ -35,8 +34,6 @@ pub(crate) fn make_acme_dir() -> Result<(), Error> { create_acme_subdir(ACME_DIR) } -pub const DEFAULT_ACME_DIRECTORY_ENTRY: &KnownAcmeDirectory = &KNOWN_ACME_DIRECTORIES[0]; - pub fn foreach_acme_account(mut func: F) -> Result<(), Error> where F: FnMut(AcmeAccountName) -> ControlFlow>, @@ -80,7 +77,7 @@ pub fn load_dns_challenge_schema() -> Result, Error> { .and_then(Value::as_str) .unwrap_or(id) .to_owned(), - ty: "dns", + ty: "dns".into(), schema: schema.to_owned(), }) .collect()) diff --git a/src/config/acme/plugin.rs b/src/config/acme/plugin.rs index 18e71199..2e979ffe 100644 --- a/src/config/acme/plugin.rs +++ b/src/config/acme/plugin.rs @@ -1,104 +1,15 @@ use std::sync::LazyLock; use anyhow::Error; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use proxmox_schema::{api, ApiType, Schema, StringSchema, Updater}; -use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin}; - -use pbs_api_types::PROXMOX_SAFE_ID_FORMAT; use pbs_config::{open_backup_lockfile, BackupLockGuard}; - -pub const PLUGIN_ID_SCHEMA: Schema = StringSchema::new("ACME Challenge Plugin ID.") - .format(&PROXMOX_SAFE_ID_FORMAT) - .min_length(1) - .max_length(32) - .schema(); +use proxmox_acme_api::PLUGIN_ID_SCHEMA; +use proxmox_acme_api::{DnsPlugin, StandalonePlugin}; +use proxmox_schema::{ApiType, Schema}; +use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin}; +use serde_json::Value; pub static CONFIG: LazyLock = LazyLock::new(init); -#[api( - properties: { - id: { schema: PLUGIN_ID_SCHEMA }, - }, -)] -#[derive(Deserialize, Serialize)] -/// Standalone ACME Plugin for the http-1 challenge. -pub struct StandalonePlugin { - /// Plugin ID. - id: String, -} - -impl Default for StandalonePlugin { - fn default() -> Self { - Self { - id: "standalone".to_string(), - } - } -} - -#[api( - properties: { - id: { schema: PLUGIN_ID_SCHEMA }, - disable: { - optional: true, - default: false, - }, - "validation-delay": { - default: 30, - optional: true, - minimum: 0, - maximum: 2 * 24 * 60 * 60, - }, - }, -)] -/// DNS ACME Challenge Plugin core data. -#[derive(Deserialize, Serialize, Updater)] -#[serde(rename_all = "kebab-case")] -pub struct DnsPluginCore { - /// Plugin ID. - #[updater(skip)] - pub id: String, - - /// DNS API Plugin Id. - pub api: String, - - /// Extra delay in seconds to wait before requesting validation. - /// - /// Allows to cope with long TTL of DNS records. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub validation_delay: Option, - - /// Flag to disable the config. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub disable: Option, -} - -#[api( - properties: { - core: { type: DnsPluginCore }, - }, -)] -/// DNS ACME Challenge Plugin. -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct DnsPlugin { - #[serde(flatten)] - pub core: DnsPluginCore, - - // We handle this property separately in the API calls. - /// DNS plugin data (base64url encoded without padding). - #[serde(with = "proxmox_serde::string_as_base64url_nopad")] - pub data: String, -} - -impl DnsPlugin { - pub fn decode_data(&self, output: &mut Vec) -> Result<(), Error> { - Ok(proxmox_base64::url::decode_to_vec(&self.data, output)?) - } -} - fn init() -> SectionConfig { let mut config = SectionConfig::new(&PLUGIN_ID_SCHEMA); diff --git a/src/config/node.rs b/src/config/node.rs index d2a17a49..b9257adf 100644 --- a/src/config/node.rs +++ b/src/config/node.rs @@ -6,17 +6,17 @@ use serde::{Deserialize, Serialize}; use proxmox_schema::{api, ApiStringFormat, ApiType, Updater}; -use proxmox_http::ProxyConfig; - use pbs_api_types::{ EMAIL_SCHEMA, MULTI_LINE_COMMENT_SCHEMA, OPENSSL_CIPHERS_TLS_1_2_SCHEMA, OPENSSL_CIPHERS_TLS_1_3_SCHEMA, }; +use proxmox_acme_api::{AcmeConfig, AcmeDomain, ACME_DOMAIN_PROPERTY_SCHEMA}; +use proxmox_http::ProxyConfig; use pbs_buildcfg::configdir; use pbs_config::{open_backup_lockfile, BackupLockGuard}; -use crate::api2::types::{AcmeDomain, ACME_DOMAIN_PROPERTY_SCHEMA, HTTP_PROXY_SCHEMA}; +use crate::api2::types::HTTP_PROXY_SCHEMA; use proxmox_acme::async_client::AcmeClient; use proxmox_acme_api::AcmeAccountName; @@ -45,20 +45,6 @@ pub fn save_config(config: &NodeConfig) -> Result<(), Error> { pbs_config::replace_backup_config(CONF_FILE, &raw) } -#[api( - properties: { - account: { type: AcmeAccountName }, - } -)] -#[derive(Deserialize, Serialize)] -/// The ACME configuration. -/// -/// Currently only contains the name of the account use. -pub struct AcmeConfig { - /// Account to use to acquire ACME certificates. - account: AcmeAccountName, -} - /// All available languages in Proxmox. Taken from proxmox-i18n repository. /// pt_BR, zh_CN, and zh_TW use the same case in the translation files. // TODO: auto-generate from available translations @@ -244,7 +230,7 @@ impl NodeConfig { pub async fn acme_client(&self) -> Result { let account = if let Some(cfg) = self.acme_config().transpose()? { - cfg.account + AcmeAccountName::from_string(cfg.account)? } else { AcmeAccountName::from_string("default".to_string())? // should really not happen }; diff --git a/src/lib.rs b/src/lib.rs index 8633378c..828f5842 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,8 +27,6 @@ pub(crate) mod auth; pub mod tape; -pub mod acme; - pub mod client_helpers; pub mod traffic_control_cache; -- 2.47.3 _______________________________________________ pbs-devel mailing list pbs-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel