1 use std
::future
::Future
;
3 use std
::process
::Stdio
;
5 use std
::time
::Duration
;
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
;
12 use proxmox_acme_rs
::{Authorization, Challenge}
;
14 use crate::acme
::AcmeClient
;
15 use crate::api2
::types
::AcmeDomain
;
16 use proxmox_rest_server
::WorkerTask
;
18 use crate::config
::acme
::plugin
::{DnsPlugin, PluginData}
;
20 const PROXMOX_ACME_SH_PATH
: &str = "/usr/share/proxmox-acme/proxmox-acme";
22 pub(crate) fn get_acme_plugin(
23 plugin_data
: &PluginData
,
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
),
31 Ok(Some(match ty
.as_str() {
33 let plugin
: DnsPlugin
= serde_json
::from_value(data
.clone())?
;
37 // this one has no config
38 Box
::new(StandaloneServer
::default())
40 other
=> bail
!("missing implementation for plugin type '{}'", other
),
44 pub(crate) trait AcmePlugin
{
45 /// Setup everything required to trigger the validation and return the corresponding validation
47 fn setup
<'fut
, 'a
: 'fut
, 'b
: 'fut
, 'c
: 'fut
, 'd
: 'fut
>(
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
>>;
55 fn teardown
<'fut
, 'a
: 'fut
, 'b
: 'fut
, 'c
: 'fut
, 'd
: 'fut
>(
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
>>;
64 fn extract_challenge
<'a
>(
65 authorization
: &'a Authorization
,
67 ) -> Result
<&'a Challenge
, Error
> {
71 .find(|ch
| ch
.ty
== ty
)
72 .ok_or_else(|| format_err
!("no supported challenge type ({}) found", ty
))
75 async
fn pipe_to_tasklog
<T
: AsyncRead
+ Unpin
>(
77 task
: Arc
<WorkerTask
>,
78 ) -> Result
<(), std
::io
::Error
> {
79 let mut pipe
= BufReader
::new(pipe
);
80 let mut line
= String
::new();
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
),
94 client
: &mut AcmeClient
,
95 authorization
: &'a Authorization
,
97 task
: Arc
<WorkerTask
>,
99 ) -> Result
<&'a
str, Error
> {
100 let challenge
= extract_challenge(authorization
, "dns-01")?
;
101 let mut stdin_data
= client
105 .ok_or_else(|| format_err
!("missing token in challenge"))?
,
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'
);
114 let mut command
= Command
::new("/usr/bin/setpriv");
119 "--regid", "nogroup",
124 PROXMOX_ACME_SH_PATH
,
127 domain
.alias
.as_deref().unwrap_or(&domain
.domain
),
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 :-(
133 let mut child
= command
134 .stdin(Stdio
::piped())
135 .stdout(Stdio
::piped())
136 .stderr(Stdio
::piped())
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
>(())
149 match futures
::try_join
!(stdin
, stdout
, stderr
) {
150 Ok(((), (), ())) => (),
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
158 bail
!("'{}' failed: {}", PROXMOX_ACME_SH_PATH
, err
);
162 let status
= child
.wait().await?
;
163 if !status
.success() {
165 "'{} {}' exited with error ({})",
166 PROXMOX_ACME_SH_PATH
,
168 status
.code().unwrap_or(-1)
176 impl AcmePlugin
for DnsPlugin
{
177 fn setup
<'fut
, 'a
: 'fut
, 'b
: 'fut
, 'c
: 'fut
, 'd
: 'fut
>(
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 {
186 .action(client
, authorization
, domain
, task
.clone(), "setup")
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",
195 tokio
::time
::sleep(Duration
::from_secs(validation_delay
)).await
;
201 fn teardown
<'fut
, 'a
: 'fut
, 'b
: 'fut
, 'c
: 'fut
, 'd
: 'fut
>(
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")
217 struct StandaloneServer
{
218 abort_handle
: Option
<futures
::future
::AbortHandle
>,
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
{
229 impl StandaloneServer
{
231 if let Some(abort
) = self.abort_handle
.take() {
237 async
fn standalone_respond(
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())
248 Ok(Response
::builder()
249 .status(http
::StatusCode
::NOT_FOUND
)
250 .body("Not found.".into())
255 impl AcmePlugin
for StandaloneServer
{
256 fn setup
<'fut
, 'a
: 'fut
, 'b
: 'fut
, 'c
: 'fut
, 'd
: 'fut
>(
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}
;
266 Box
::pin(async
move {
269 let challenge
= extract_challenge(authorization
, "http-01")?
;
270 let token
= challenge
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
));
276 let service
= make_service_fn(move |_
| {
277 let path
= Arc
::clone(&path
);
278 let key_auth
= Arc
::clone(&key_auth
);
280 Ok
::<_
, hyper
::Error
>(service_fn(move |request
| {
281 standalone_respond(request
, Arc
::clone(&path
), Arc
::clone(&key_auth
))
286 // `[::]:80` first, then `*:80`
287 let incoming
= AddrIncoming
::bind(&(([0u16; 8], 80).into()))
288 .or_else(|_
| AddrIncoming
::bind(&(([0u8; 4], 80).into())))?
;
290 let server
= hyper
::Server
::builder(incoming
).serve(service
);
292 let (future
, abort
) = futures
::future
::abortable(server
);
293 self.abort_handle
= Some(abort
);
294 tokio
::spawn(future
);
296 Ok(challenge
.url
.as_str())
300 fn teardown
<'fut
, 'a
: 'fut
, 'b
: 'fut
, 'c
: 'fut
, 'd
: 'fut
>(
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() {