From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 7CD691FF146 for ; Fri, 16 Jan 2026 12:29:38 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id B51FA17769; Fri, 16 Jan 2026 12:29:43 +0100 (CET) From: Samuel Rufinatscha To: pbs-devel@lists.proxmox.com Date: Fri, 16 Jan 2026 12:28:59 +0100 Message-ID: <20260116112859.194016-6-s.rufinatscha@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260116112859.194016-1-s.rufinatscha@proxmox.com> References: <20260116112859.194016-1-s.rufinatscha@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1768562898460 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.253 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 v6 2/2] acme: remove unused src/acme and plugin code 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" Removes the unused src/acme module and plugin code as PBS now uses the factored out client/API handlers. Signed-off-by: Samuel Rufinatscha --- src/acme/mod.rs | 1 - src/acme/plugin.rs | 335 -------------------------------------- src/api2/types/acme.rs | 38 ----- src/api2/types/mod.rs | 3 - src/config/acme/mod.rs | 1 - src/config/acme/plugin.rs | 105 ------------ src/config/mod.rs | 1 - src/lib.rs | 2 - 8 files changed, 486 deletions(-) delete mode 100644 src/acme/mod.rs delete mode 100644 src/acme/plugin.rs delete mode 100644 src/api2/types/acme.rs delete mode 100644 src/config/acme/mod.rs delete mode 100644 src/config/acme/plugin.rs diff --git a/src/acme/mod.rs b/src/acme/mod.rs deleted file mode 100644 index 700d90d7..00000000 --- a/src/acme/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod plugin; diff --git a/src/acme/plugin.rs b/src/acme/plugin.rs deleted file mode 100644 index 6804243c..00000000 --- a/src/acme/plugin.rs +++ /dev/null @@ -1,335 +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::async_client::AcmeClient; -use proxmox_acme::{Authorization, Challenge}; -use proxmox_rest_server::WorkerTask; - -use crate::api2::types::AcmeDomain; -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/types/acme.rs b/src/api2/types/acme.rs deleted file mode 100644 index b83b9882..00000000 --- a/src/api2/types/acme.rs +++ /dev/null @@ -1,38 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use pbs_api_types::{DNS_ALIAS_FORMAT, DNS_NAME_FORMAT, PROXMOX_SAFE_ID_FORMAT}; -use proxmox_schema::api; - -#[api( - properties: { - "domain": { format: &DNS_NAME_FORMAT }, - "alias": { - optional: true, - format: &DNS_ALIAS_FORMAT, - }, - "plugin": { - optional: true, - format: &PROXMOX_SAFE_ID_FORMAT, - }, - }, - default_key: "domain", -)] -#[derive(Deserialize, Serialize)] -/// A domain entry for an ACME certificate. -pub struct AcmeDomain { - /// The domain to certify for. - pub domain: String, - - /// The domain to use for challenges instead of the default acme challenge domain. - /// - /// This is useful if you use CNAME entries to redirect `_acme-challenge.*` domains to a - /// different DNS server. - #[serde(skip_serializing_if = "Option::is_none")] - pub alias: Option, - - /// 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, -} 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 deleted file mode 100644 index 962cb1bb..00000000 --- a/src/config/acme/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod plugin; diff --git a/src/config/acme/plugin.rs b/src/config/acme/plugin.rs deleted file mode 100644 index e5a41f99..00000000 --- a/src/config/acme/plugin.rs +++ /dev/null @@ -1,105 +0,0 @@ -use anyhow::Error; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use pbs_api_types::PROXMOX_SAFE_ID_FORMAT; -use proxmox_schema::{api, Schema, StringSchema, Updater}; -use proxmox_section_config::SectionConfigData; - -pub const PLUGIN_ID_SCHEMA: Schema = StringSchema::new("ACME Challenge Plugin ID.") - .format(&PROXMOX_SAFE_ID_FORMAT) - .min_length(1) - .max_length(32) - .schema(); - -#[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)?) - } -} - -pub struct PluginData { - data: SectionConfigData, -} - -// And some convenience helpers. -impl PluginData { - pub fn remove(&mut self, name: &str) -> Option<(String, Value)> { - self.data.sections.remove(name) - } - - pub fn contains_key(&mut self, name: &str) -> bool { - self.data.sections.contains_key(name) - } - - pub fn get(&self, name: &str) -> Option<&(String, Value)> { - self.data.sections.get(name) - } - - pub fn get_mut(&mut self, name: &str) -> Option<&mut (String, Value)> { - self.data.sections.get_mut(name) - } - - pub fn insert(&mut self, id: String, ty: String, plugin: Value) { - self.data.sections.insert(id, (ty, plugin)); - } - - pub fn iter(&self) -> impl Iterator + Send { - self.data.sections.iter() - } -} diff --git a/src/config/mod.rs b/src/config/mod.rs index 19246742..f05af90d 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -15,7 +15,6 @@ use proxmox_lang::try_block; use pbs_api_types::{PamRealmConfig, PbsRealmConfig}; use pbs_buildcfg::{self, configdir}; -pub mod acme; pub mod node; pub mod tfa; 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