From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 9EBD073ADF for ; Fri, 16 Apr 2021 15:35:54 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 3981524FDB for ; Fri, 16 Apr 2021 15:35:33 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 8C4AD24CA1 for ; Fri, 16 Apr 2021 15:35:24 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 5086845B1C for ; Fri, 16 Apr 2021 15:35:24 +0200 (CEST) From: Wolfgang Bumiller To: pbs-devel@lists.proxmox.com Date: Fri, 16 Apr 2021 15:35:14 +0200 Message-Id: <20210416133517.23349-22-w.bumiller@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20210416133517.23349-1-w.bumiller@proxmox.com> References: <20210416133517.23349-1-w.bumiller@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.031 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [plugin.rs, certificates.rs] Subject: [pbs-devel] [RFC backup 21/23] implement standalone acme validation 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: , X-List-Received-Date: Fri, 16 Apr 2021 13:35:54 -0000 Signed-off-by: Wolfgang Bumiller --- src/api2/node/certificates.rs | 2 +- src/config/acme/plugin.rs | 144 ++++++++++++++++++++++++++++++---- 2 files changed, 129 insertions(+), 17 deletions(-) diff --git a/src/api2/node/certificates.rs b/src/api2/node/certificates.rs index 4dd75027..f6da31ec 100644 --- a/src/api2/node/certificates.rs +++ b/src/api2/node/certificates.rs @@ -342,7 +342,7 @@ async fn order_certificate( worker.log(format!("The validation for {} is pending", domain)); let domain_config: &AcmeDomain = get_domain_config(&domain)?; let plugin_id = domain_config.plugin.as_deref().unwrap_or("standalone"); - let plugin_cfg = plugins.get_plugin(plugin_id)?.ok_or_else(|| { + let mut plugin_cfg = plugins.get_plugin(plugin_id)?.ok_or_else(|| { format_err!("plugin '{}' for domain '{}' not found!", plugin_id, domain) })?; diff --git a/src/config/acme/plugin.rs b/src/config/acme/plugin.rs index b0c655c0..a2fa0490 100644 --- a/src/config/acme/plugin.rs +++ b/src/config/acme/plugin.rs @@ -1,8 +1,10 @@ use std::future::Future; use std::pin::Pin; use std::process::Stdio; +use std::sync::Arc; use anyhow::{bail, format_err, Error}; +use hyper::{Body, Request, Response}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -117,7 +119,11 @@ pub struct DnsPlugin { impl DnsPlugin { pub fn decode_data(&self, output: &mut Vec) -> Result<(), Error> { - Ok(base64::decode_config_buf(&self.data, base64::URL_SAFE_NO_PAD, output)?) + Ok(base64::decode_config_buf( + &self.data, + base64::URL_SAFE_NO_PAD, + output, + )?) } } @@ -250,7 +256,10 @@ impl PluginData { let plugin: DnsPlugin = serde_json::from_value(data.clone())?; Box::new(plugin) } - // "standalone" => todo!("standalone plugin"), + "standalone" => { + // this one has no config + Box::new(StandaloneServer::default()) + } other => bail!("missing implementation for plugin type '{}'", other), })) } @@ -264,29 +273,32 @@ pub 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 self, + &'a mut self, client: &'b mut AcmeClient, authorization: &'c Authorization, domain: &'d AcmeDomain, ) -> Pin> + Send + 'fut>>; fn teardown<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>( - &'a self, + &'a mut self, client: &'b mut AcmeClient, authorization: &'c Authorization, domain: &'d AcmeDomain, ) -> Pin> + Send + 'fut>>; } -impl DnsPlugin { - fn extract_challenge(authorization: &Authorization) -> Result<&Challenge, Error> { - authorization - .challenges - .iter() - .find(|ch| ch.ty == "dns-01") - .ok_or_else(|| format_err!("no supported challenge type (dns-01) found")) - } +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 (dns-01) found")) +} +impl DnsPlugin { async fn action<'a>( &self, client: &mut AcmeClient, @@ -294,7 +306,7 @@ impl DnsPlugin { domain: &AcmeDomain, action: &str, ) -> Result<&'a str, Error> { - let challenge = Self::extract_challenge(authorization)?; + let challenge = extract_challenge(authorization, "dns-01")?; let mut stdin_data = client .dns_01_txt_value( challenge @@ -331,7 +343,9 @@ impl DnsPlugin { stdin.write_all(&stdin_data).await?; stdin.flush().await?; Ok::<_, std::io::Error>(()) - }.await { + } + .await + { Ok(()) => (), Err(err) => { if let Err(err) = child.kill().await { @@ -357,7 +371,7 @@ impl DnsPlugin { impl AcmePlugin for DnsPlugin { fn setup<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>( - &'a self, + &'a mut self, client: &'b mut AcmeClient, authorization: &'c Authorization, domain: &'d AcmeDomain, @@ -366,7 +380,7 @@ impl AcmePlugin for DnsPlugin { } fn teardown<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>( - &'a self, + &'a mut self, client: &'b mut AcmeClient, authorization: &'c Authorization, domain: &'d AcmeDomain, @@ -378,3 +392,101 @@ impl AcmePlugin for DnsPlugin { }) } } + +#[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(http::StatusCode::OK) + .body(key_auth.as_bytes().to_vec().into()) + .unwrap()) + } else { + Ok(Response::builder() + .status(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, + ) -> Pin> + Send + 'fut>> { + use hyper::server::conn::AddrIncoming; + use hyper::service::{make_service_fn, service_fn}; + + 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)); + + let service = make_service_fn(move |_| { + let path = Arc::clone(&path); + let key_auth = Arc::clone(&key_auth); + async move { + Ok::<_, hyper::Error>(service_fn(move |request| { + standalone_respond(request, Arc::clone(&path), Arc::clone(&key_auth)) + })) + } + }); + + // `[::]:80` first, then `*:80` + let incoming = AddrIncoming::bind(&(([0u16; 8], 80).into())) + .or_else(|_| AddrIncoming::bind(&(([0u8; 4], 80).into())))?; + + let server = hyper::Server::builder(incoming).serve(service); + + 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, + ) -> Pin> + Send + 'fut>> { + Box::pin(async move { + if let Some(abort) = self.abort_handle.take() { + abort.abort(); + } + Ok(()) + }) + } +} -- 2.20.1