]> git.proxmox.com Git - proxmox-backup.git/blob - src/acme/plugin.rs
tree-wide: fix needless borrows
[proxmox-backup.git] / src / acme / plugin.rs
1 use std::future::Future;
2 use std::pin::Pin;
3 use std::process::Stdio;
4 use std::sync::Arc;
5 use std::time::Duration;
6
7 use anyhow::{bail, format_err, Error};
8 use hyper::{Body, Request, Response};
9 use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWriteExt, BufReader};
10 use tokio::process::Command;
11
12 use proxmox_acme_rs::{Authorization, Challenge};
13
14 use crate::acme::AcmeClient;
15 use crate::api2::types::AcmeDomain;
16 use proxmox_rest_server::WorkerTask;
17
18 use crate::config::acme::plugin::{DnsPlugin, PluginData};
19
20 const PROXMOX_ACME_SH_PATH: &str = "/usr/share/proxmox-acme/proxmox-acme";
21
22 pub(crate) fn get_acme_plugin(
23 plugin_data: &PluginData,
24 name: &str,
25 ) -> Result<Option<Box<dyn AcmePlugin + Send + Sync + 'static>>, Error> {
26 let (ty, data) = match plugin_data.get(name) {
27 Some(plugin) => plugin,
28 None => return Ok(None),
29 };
30
31 Ok(Some(match ty.as_str() {
32 "dns" => {
33 let plugin: DnsPlugin = serde_json::from_value(data.clone())?;
34 Box::new(plugin)
35 }
36 "standalone" => {
37 // this one has no config
38 Box::new(StandaloneServer::default())
39 }
40 other => bail!("missing implementation for plugin type '{}'", other),
41 }))
42 }
43
44 pub(crate) trait AcmePlugin {
45 /// Setup everything required to trigger the validation and return the corresponding validation
46 /// URL.
47 fn setup<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>(
48 &'a mut self,
49 client: &'b mut AcmeClient,
50 authorization: &'c Authorization,
51 domain: &'d AcmeDomain,
52 task: Arc<WorkerTask>,
53 ) -> Pin<Box<dyn Future<Output = Result<&'c str, Error>> + Send + 'fut>>;
54
55 fn teardown<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>(
56 &'a mut self,
57 client: &'b mut AcmeClient,
58 authorization: &'c Authorization,
59 domain: &'d AcmeDomain,
60 task: Arc<WorkerTask>,
61 ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'fut>>;
62 }
63
64 fn extract_challenge<'a>(
65 authorization: &'a Authorization,
66 ty: &str,
67 ) -> Result<&'a Challenge, Error> {
68 authorization
69 .challenges
70 .iter()
71 .find(|ch| ch.ty == ty)
72 .ok_or_else(|| format_err!("no supported challenge type ({}) found", ty))
73 }
74
75 async fn pipe_to_tasklog<T: AsyncRead + Unpin>(
76 pipe: T,
77 task: Arc<WorkerTask>,
78 ) -> Result<(), std::io::Error> {
79 let mut pipe = BufReader::new(pipe);
80 let mut line = String::new();
81 loop {
82 line.clear();
83 match pipe.read_line(&mut line).await {
84 Ok(0) => return Ok(()),
85 Ok(_) => task.log_message(line.as_str()),
86 Err(err) => return Err(err),
87 }
88 }
89 }
90
91 impl DnsPlugin {
92 async fn action<'a>(
93 &self,
94 client: &mut AcmeClient,
95 authorization: &'a Authorization,
96 domain: &AcmeDomain,
97 task: Arc<WorkerTask>,
98 action: &str,
99 ) -> Result<&'a str, Error> {
100 let challenge = extract_challenge(authorization, "dns-01")?;
101 let mut stdin_data = client
102 .dns_01_txt_value(
103 challenge
104 .token()
105 .ok_or_else(|| format_err!("missing token in challenge"))?,
106 )?
107 .into_bytes();
108 stdin_data.push(b'\n');
109 stdin_data.extend(self.data.as_bytes());
110 if stdin_data.last() != Some(&b'\n') {
111 stdin_data.push(b'\n');
112 }
113
114 let mut command = Command::new("/usr/bin/setpriv");
115
116 #[rustfmt::skip]
117 command.args(&[
118 "--reuid", "nobody",
119 "--regid", "nogroup",
120 "--clear-groups",
121 "--reset-env",
122 "--",
123 "/bin/bash",
124 PROXMOX_ACME_SH_PATH,
125 action,
126 &self.core.api,
127 domain.alias.as_deref().unwrap_or(&domain.domain),
128 ]);
129
130 // We could use 1 socketpair, but tokio wraps them all in `File` internally causing `close`
131 // to be called separately on all of them without exception, so we need 3 pipes :-(
132
133 let mut child = command
134 .stdin(Stdio::piped())
135 .stdout(Stdio::piped())
136 .stderr(Stdio::piped())
137 .spawn()?;
138
139 let mut stdin = child.stdin.take().expect("Stdio::piped()");
140 let stdout = child.stdout.take().expect("Stdio::piped() failed?");
141 let stdout = pipe_to_tasklog(stdout, Arc::clone(&task));
142 let stderr = child.stderr.take().expect("Stdio::piped() failed?");
143 let stderr = pipe_to_tasklog(stderr, Arc::clone(&task));
144 let stdin = async move {
145 stdin.write_all(&stdin_data).await?;
146 stdin.flush().await?;
147 Ok::<_, std::io::Error>(())
148 };
149 match futures::try_join!(stdin, stdout, stderr) {
150 Ok(((), (), ())) => (),
151 Err(err) => {
152 if let Err(err) = child.kill().await {
153 task.log_message(format!(
154 "failed to kill '{} {}' command: {}",
155 PROXMOX_ACME_SH_PATH, action, err
156 ));
157 }
158 bail!("'{}' failed: {}", PROXMOX_ACME_SH_PATH, err);
159 }
160 }
161
162 let status = child.wait().await?;
163 if !status.success() {
164 bail!(
165 "'{} {}' exited with error ({})",
166 PROXMOX_ACME_SH_PATH,
167 action,
168 status.code().unwrap_or(-1)
169 );
170 }
171
172 Ok(&challenge.url)
173 }
174 }
175
176 impl AcmePlugin for DnsPlugin {
177 fn setup<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>(
178 &'a mut self,
179 client: &'b mut AcmeClient,
180 authorization: &'c Authorization,
181 domain: &'d AcmeDomain,
182 task: Arc<WorkerTask>,
183 ) -> Pin<Box<dyn Future<Output = Result<&'c str, Error>> + Send + 'fut>> {
184 Box::pin(async move {
185 let result = self
186 .action(client, authorization, domain, task.clone(), "setup")
187 .await;
188
189 let validation_delay = self.core.validation_delay.unwrap_or(30) as u64;
190 if validation_delay > 0 {
191 task.log_message(format!(
192 "Sleeping {} seconds to wait for TXT record propagation",
193 validation_delay
194 ));
195 tokio::time::sleep(Duration::from_secs(validation_delay)).await;
196 }
197 result
198 })
199 }
200
201 fn teardown<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>(
202 &'a mut self,
203 client: &'b mut AcmeClient,
204 authorization: &'c Authorization,
205 domain: &'d AcmeDomain,
206 task: Arc<WorkerTask>,
207 ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'fut>> {
208 Box::pin(async move {
209 self.action(client, authorization, domain, task, "teardown")
210 .await
211 .map(drop)
212 })
213 }
214 }
215
216 #[derive(Default)]
217 struct StandaloneServer {
218 abort_handle: Option<futures::future::AbortHandle>,
219 }
220
221 // In case the "order_certificates" future gets dropped between setup & teardown, let's also cancel
222 // the HTTP listener on Drop:
223 impl Drop for StandaloneServer {
224 fn drop(&mut self) {
225 self.stop();
226 }
227 }
228
229 impl StandaloneServer {
230 fn stop(&mut self) {
231 if let Some(abort) = self.abort_handle.take() {
232 abort.abort();
233 }
234 }
235 }
236
237 async fn standalone_respond(
238 req: Request<Body>,
239 path: Arc<String>,
240 key_auth: Arc<String>,
241 ) -> Result<Response<Body>, hyper::Error> {
242 if req.method() == hyper::Method::GET && req.uri().path() == path.as_str() {
243 Ok(Response::builder()
244 .status(http::StatusCode::OK)
245 .body(key_auth.as_bytes().to_vec().into())
246 .unwrap())
247 } else {
248 Ok(Response::builder()
249 .status(http::StatusCode::NOT_FOUND)
250 .body("Not found.".into())
251 .unwrap())
252 }
253 }
254
255 impl AcmePlugin for StandaloneServer {
256 fn setup<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>(
257 &'a mut self,
258 client: &'b mut AcmeClient,
259 authorization: &'c Authorization,
260 _domain: &'d AcmeDomain,
261 _task: Arc<WorkerTask>,
262 ) -> Pin<Box<dyn Future<Output = Result<&'c str, Error>> + Send + 'fut>> {
263 use hyper::server::conn::AddrIncoming;
264 use hyper::service::{make_service_fn, service_fn};
265
266 Box::pin(async move {
267 self.stop();
268
269 let challenge = extract_challenge(authorization, "http-01")?;
270 let token = challenge
271 .token()
272 .ok_or_else(|| format_err!("missing token in challenge"))?;
273 let key_auth = Arc::new(client.key_authorization(token)?);
274 let path = Arc::new(format!("/.well-known/acme-challenge/{}", token));
275
276 let service = make_service_fn(move |_| {
277 let path = Arc::clone(&path);
278 let key_auth = Arc::clone(&key_auth);
279 async move {
280 Ok::<_, hyper::Error>(service_fn(move |request| {
281 standalone_respond(request, Arc::clone(&path), Arc::clone(&key_auth))
282 }))
283 }
284 });
285
286 // `[::]:80` first, then `*:80`
287 let incoming = AddrIncoming::bind(&(([0u16; 8], 80).into()))
288 .or_else(|_| AddrIncoming::bind(&(([0u8; 4], 80).into())))?;
289
290 let server = hyper::Server::builder(incoming).serve(service);
291
292 let (future, abort) = futures::future::abortable(server);
293 self.abort_handle = Some(abort);
294 tokio::spawn(future);
295
296 Ok(challenge.url.as_str())
297 })
298 }
299
300 fn teardown<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>(
301 &'a mut self,
302 _client: &'b mut AcmeClient,
303 _authorization: &'c Authorization,
304 _domain: &'d AcmeDomain,
305 _task: Arc<WorkerTask>,
306 ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'fut>> {
307 Box::pin(async move {
308 if let Some(abort) = self.abort_handle.take() {
309 abort.abort();
310 }
311 Ok(())
312 })
313 }
314 }