3 use std
::sync
::{Arc, Mutex}
;
4 use std
::time
::SystemTime
;
6 use anyhow
::{bail, format_err, Error}
;
7 use lazy_static
::lazy_static
;
8 use serde
::{Deserialize, Serialize}
;
9 use serde_json
::{json, Value}
;
11 use proxmox
::api
::router
::SubdirMap
;
12 use proxmox
::api
::{api, Permission, Router, RpcEnvironment}
;
13 use proxmox
::http_bail
;
14 use proxmox
::list_subdirs_api_method
;
16 use proxmox_acme_rs
::account
::AccountData
as AcmeAccountData
;
17 use proxmox_acme_rs
::Account
;
19 use pbs_api_types
::{Authid, PRIV_SYS_MODIFY}
;
21 use crate::acme
::AcmeClient
;
22 use crate::api2
::types
::{AcmeAccountName, AcmeChallengeSchema, KnownAcmeDirectory}
;
23 use crate::config
::acme
::plugin
::{
24 self, DnsPlugin
, DnsPluginCore
, DnsPluginCoreUpdater
, PLUGIN_ID_SCHEMA
,
26 use crate::server
::WorkerTask
;
27 use crate::tools
::ControlFlow
;
29 pub(crate) const ROUTER
: Router
= Router
::new()
30 .get(&list_subdirs_api_method
!(SUBDIRS
))
33 const SUBDIRS
: SubdirMap
= &[
37 .get(&API_METHOD_LIST_ACCOUNTS
)
38 .post(&API_METHOD_REGISTER_ACCOUNT
)
39 .match_all("name", &ACCOUNT_ITEM_ROUTER
),
43 &Router
::new().get(&API_METHOD_GET_CHALLENGE_SCHEMA
),
47 &Router
::new().get(&API_METHOD_GET_DIRECTORIES
),
52 .get(&API_METHOD_LIST_PLUGINS
)
53 .post(&API_METHOD_ADD_PLUGIN
)
54 .match_all("id", &PLUGIN_ITEM_ROUTER
),
56 ("tos", &Router
::new().get(&API_METHOD_GET_TOS
)),
59 const ACCOUNT_ITEM_ROUTER
: Router
= Router
::new()
60 .get(&API_METHOD_GET_ACCOUNT
)
61 .put(&API_METHOD_UPDATE_ACCOUNT
)
62 .delete(&API_METHOD_DEACTIVATE_ACCOUNT
);
64 const PLUGIN_ITEM_ROUTER
: Router
= Router
::new()
65 .get(&API_METHOD_GET_PLUGIN
)
66 .put(&API_METHOD_UPDATE_PLUGIN
)
67 .delete(&API_METHOD_DELETE_PLUGIN
);
71 name
: { type: AcmeAccountName }
,
74 /// An ACME Account entry.
76 /// Currently only contains a 'name' property.
78 pub struct AccountEntry
{
79 name
: AcmeAccountName
,
84 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
88 items
: { type: AccountEntry }
,
89 description
: "List of ACME accounts.",
93 /// List ACME accounts.
94 pub fn list_accounts() -> Result
<Vec
<AccountEntry
>, Error
> {
95 let mut entries
= Vec
::new();
96 crate::config
::acme
::foreach_acme_account(|name
| {
97 entries
.push(AccountEntry { name }
);
98 ControlFlow
::Continue(())
105 account
: { type: Object, properties: {}
, additional_properties
: true },
112 /// ACME Account information.
114 /// This is what we return via the API.
116 pub struct AccountInfo
{
117 /// Raw account data.
118 account
: AcmeAccountData
,
120 /// The ACME directory URL the account was created at.
123 /// The account's own URL within the ACME directory.
126 /// The ToS URL, if the user agreed to one.
127 #[serde(skip_serializing_if = "Option::is_none")]
134 name
: { type: AcmeAccountName }
,
138 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
140 returns
: { type: AccountInfo }
,
143 /// Return existing ACME account information.
144 pub async
fn get_account(name
: AcmeAccountName
) -> Result
<AccountInfo
, Error
> {
145 let client
= AcmeClient
::load(&name
).await?
;
146 let account
= client
.account()?
;
148 location
: account
.location
.clone(),
149 tos
: client
.tos().map(str::to_owned
),
150 directory
: client
.directory_url().to_owned(),
151 account
: AcmeAccountData
{
152 only_return_existing
: false, // don't actually write this out in case it's set
153 ..account
.data
.clone()
158 fn account_contact_from_string(s
: &str) -> Vec
<String
> {
159 s
.split(&[' '
, '
;'
, '
,'
, '
\0'
][..])
160 .map(|s
| format
!("mailto:{}", s
))
168 type: AcmeAccountName
,
172 description
: "List of email addresses.",
175 description
: "URL of CA TermsOfService - setting this indicates agreement.",
180 description
: "The ACME Directory.",
186 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
190 /// Register an ACME account.
192 name
: Option
<AcmeAccountName
>,
193 // Todo: email & email-list schema
195 tos_url
: Option
<String
>,
196 directory
: Option
<String
>,
197 rpcenv
: &mut dyn RpcEnvironment
,
198 ) -> Result
<String
, Error
> {
199 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
201 let name
= name
.unwrap_or_else(|| unsafe {
202 AcmeAccountName
::from_string_unchecked("default".to_string())
205 if Path
::new(&crate::config
::acme
::account_path(&name
)).exists() {
206 http_bail
!(BAD_REQUEST
, "account {} already exists", name
);
209 let directory
= directory
.unwrap_or_else(|| {
210 crate::config
::acme
::DEFAULT_ACME_DIRECTORY_ENTRY
217 Some(name
.to_string()),
220 move |worker
| async
move {
221 let mut client
= AcmeClient
::new(directory
);
223 worker
.log(format
!("Registering ACME account '{}'...", &name
));
226 do_register_account(&mut client
, &name
, tos_url
.is_some(), contact
, None
).await?
;
229 "Registration successful, account URL: {}",
238 pub async
fn do_register_account
<'a
>(
239 client
: &'a
mut AcmeClient
,
240 name
: &AcmeAccountName
,
243 rsa_bits
: Option
<u32>,
244 ) -> Result
<&'a Account
, Error
> {
245 let contact
= account_contact_from_string(&contact
);
247 .new_account(name
, agree_to_tos
, contact
, rsa_bits
)
254 name
: { type: AcmeAccountName }
,
256 description
: "List of email addresses.",
262 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
266 /// Update an ACME account.
267 pub fn update_account(
268 name
: AcmeAccountName
,
269 // Todo: email & email-list schema
270 contact
: Option
<String
>,
271 rpcenv
: &mut dyn RpcEnvironment
,
272 ) -> Result
<String
, Error
> {
273 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
277 Some(name
.to_string()),
280 move |_worker
| async
move {
281 let data
= match contact
{
282 Some(data
) => json
!({
283 "contact": account_contact_from_string(&data
),
288 AcmeClient
::load(&name
).await?
.update_account(&data
).await?
;
298 name
: { type: AcmeAccountName }
,
301 "Delete account data even if the server refuses to deactivate the account.",
308 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
312 /// Deactivate an ACME account.
313 pub fn deactivate_account(
314 name
: AcmeAccountName
,
316 rpcenv
: &mut dyn RpcEnvironment
,
317 ) -> Result
<String
, Error
> {
318 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
322 Some(name
.to_string()),
325 move |worker
| async
move {
326 match AcmeClient
::load(&name
)
328 .update_account(&json
!({"status": "deactivated"}
))
332 Err(err
) if !force
=> return Err(err
),
335 "error deactivating account {}, proceedeing anyway - {}",
340 crate::config
::acme
::mark_account_deactivated(&name
)?
;
351 description
: "The ACME Directory.",
357 permission
: &Permission
::Anybody
,
362 description
: "The ACME Directory's ToS URL, if any.",
365 /// Get the Terms of Service URL for an ACME directory.
366 async
fn get_tos(directory
: Option
<String
>) -> Result
<Option
<String
>, Error
> {
367 let directory
= directory
.unwrap_or_else(|| {
368 crate::config
::acme
::DEFAULT_ACME_DIRECTORY_ENTRY
372 Ok(AcmeClient
::new(directory
)
373 .terms_of_service_url()
380 permission
: &Permission
::Anybody
,
383 description
: "List of known ACME directories.",
385 items
: { type: KnownAcmeDirectory }
,
388 /// Get named known ACME directory endpoints.
389 fn get_directories() -> Result
<&'
static [KnownAcmeDirectory
], Error
> {
390 Ok(crate::config
::acme
::KNOWN_ACME_DIRECTORIES
)
393 /// Wrapper for efficient Arc use when returning the ACME challenge-plugin schema for serializing
394 struct ChallengeSchemaWrapper
{
395 inner
: Arc
<Vec
<AcmeChallengeSchema
>>,
398 impl Serialize
for ChallengeSchemaWrapper
{
399 fn serialize
<S
>(&self, serializer
: S
) -> Result
<S
::Ok
, S
::Error
>
401 S
: serde
::Serializer
,
403 self.inner
.serialize(serializer
)
407 fn get_cached_challenge_schemas() -> Result
<ChallengeSchemaWrapper
, Error
> {
409 static ref CACHE
: Mutex
<Option
<(Arc
<Vec
<AcmeChallengeSchema
>>, SystemTime
)>> =
413 // the actual loading code
414 let mut last
= CACHE
.lock().unwrap();
416 let actual_mtime
= fs
::metadata(crate::config
::acme
::ACME_DNS_SCHEMA_FN
)?
.modified()?
;
418 let schema
= match &*last
{
419 Some((schema
, cached_mtime
)) if *cached_mtime
>= actual_mtime
=> schema
.clone(),
421 let new_schema
= Arc
::new(crate::config
::acme
::load_dns_challenge_schema()?
);
422 *last
= Some((Arc
::clone(&new_schema
), actual_mtime
));
427 Ok(ChallengeSchemaWrapper { inner: schema }
)
432 permission
: &Permission
::Anybody
,
435 description
: "ACME Challenge Plugin Shema.",
437 items
: { type: AcmeChallengeSchema }
,
440 /// Get named known ACME directory endpoints.
441 fn get_challenge_schema() -> Result
<ChallengeSchemaWrapper
, Error
> {
442 get_cached_challenge_schemas()
446 #[derive(Default, Deserialize, Serialize)]
447 #[serde(rename_all = "kebab-case")]
448 /// The API's format is inherited from PVE/PMG:
449 pub struct PluginConfig
{
454 #[serde(rename = "type")]
460 /// Plugin configuration data.
461 data
: Option
<String
>,
463 /// Extra delay in seconds to wait before requesting validation.
465 /// Allows to cope with long TTL of DNS records.
466 #[serde(skip_serializing_if = "Option::is_none", default)]
467 alidation_delay
: Option
<u32>,
469 /// Flag to disable the config.
470 #[serde(skip_serializing_if = "Option::is_none", default)]
471 disable
: Option
<bool
>,
474 // See PMG/PVE's $modify_cfg_for_api sub
475 fn modify_cfg_for_api(id
: &str, ty
: &str, data
: &Value
) -> PluginConfig
{
476 let mut entry
= data
.clone();
478 let obj
= entry
.as_object_mut().unwrap();
480 obj
.insert("plugin".to_string(), Value
::String(id
.to_owned()));
481 obj
.insert("type".to_string(), Value
::String(ty
.to_owned()));
483 // FIXME: This needs to go once the `Updater` is fixed.
484 // None of these should be able to fail unless the user changed the files by hand, in which
485 // case we leave the unmodified string in the Value for now. This will be handled with an error
487 if let Some(Value
::String(ref mut data
)) = obj
.get_mut("data") {
488 if let Ok(new
) = base64
::decode_config(&data
, base64
::URL_SAFE_NO_PAD
) {
489 if let Ok(utf8
) = String
::from_utf8(new
) {
495 // PVE/PMG do this explicitly for ACME plugins...
496 // obj.insert("digest".to_string(), Value::String(digest.clone()));
498 serde_json
::from_value(entry
).unwrap_or_else(|_
| PluginConfig
{
499 plugin
: "*Error*".to_string(),
500 ty
: "*Error*".to_string(),
507 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
512 description
: "List of ACME plugin configurations.",
513 items
: { type: PluginConfig }
,
516 /// List ACME challenge plugins.
517 pub fn list_plugins(mut rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Vec
<PluginConfig
>, Error
> {
518 let (plugins
, digest
) = plugin
::config()?
;
519 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
522 .map(|(id
, (ty
, data
))| modify_cfg_for_api(&id
, &ty
, data
))
529 id
: { schema: PLUGIN_ID_SCHEMA }
,
533 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
536 returns
: { type: PluginConfig }
,
538 /// List ACME challenge plugins.
539 pub fn get_plugin(id
: String
, mut rpcenv
: &mut dyn RpcEnvironment
) -> Result
<PluginConfig
, Error
> {
540 let (plugins
, digest
) = plugin
::config()?
;
541 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
543 match plugins
.get(&id
) {
544 Some((ty
, data
)) => Ok(modify_cfg_for_api(&id
, &ty
, &data
)),
545 None
=> http_bail
!(NOT_FOUND
, "no such plugin"),
549 // Currently we only have "the" standalone plugin and DNS plugins so we can just flatten a
552 // FIXME: The 'id' parameter should not be "optional" in the schema.
558 description
: "The ACME challenge plugin type.",
566 // This is different in the API!
567 description
: "DNS plugin data (base64 encoded with padding).",
572 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
576 /// Add ACME plugin configuration.
577 pub fn add_plugin(r
#type: String, core: DnsPluginCore, data: String) -> Result<(), Error> {
578 // Currently we only support DNS plugins and the standalone plugin is "fixed":
580 bail
!("invalid ACME plugin type: {:?}", r
#type);
583 let data
= String
::from_utf8(base64
::decode(&data
)?
)
584 .map_err(|_
| format_err
!("data must be valid UTF-8"))?
;
586 let id
= core
.id
.clone();
588 let _lock
= plugin
::lock()?
;
590 let (mut plugins
, _digest
) = plugin
::config()?
;
591 if plugins
.contains_key(&id
) {
592 bail
!("ACME plugin ID {:?} already exists", id
);
595 let plugin
= serde_json
::to_value(DnsPlugin { core, data }
)?
;
597 plugins
.insert(id
, r
#type, plugin);
599 plugin
::save_config(&plugins
)?
;
607 id
: { schema: PLUGIN_ID_SCHEMA }
,
611 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
615 /// Delete an ACME plugin configuration.
616 pub fn delete_plugin(id
: String
) -> Result
<(), Error
> {
617 let _lock
= plugin
::lock()?
;
619 let (mut plugins
, _digest
) = plugin
::config()?
;
620 if plugins
.remove(&id
).is_none() {
621 http_bail
!(NOT_FOUND
, "no such plugin");
623 plugin
::save_config(&plugins
)?
;
629 #[derive(Serialize, Deserialize)]
630 #[serde(rename_all="kebab-case")]
631 #[allow(non_camel_case_types)]
632 /// Deletable property name
633 pub enum DeletableProperty
{
634 /// Delete the disable property
636 /// Delete the validation-delay property
643 id
: { schema: PLUGIN_ID_SCHEMA }
,
645 type: DnsPluginCoreUpdater
,
651 // This is different in the API!
652 description
: "DNS plugin data (base64 encoded with padding).",
655 description
: "List of properties to delete.",
659 type: DeletableProperty
,
663 description
: "Digest to protect against concurrent updates",
669 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
673 /// Update an ACME plugin configuration.
674 pub fn update_plugin(
676 update
: DnsPluginCoreUpdater
,
677 data
: Option
<String
>,
678 delete
: Option
<Vec
<DeletableProperty
>>,
679 digest
: Option
<String
>,
680 ) -> Result
<(), Error
> {
685 .map(String
::from_utf8
)
687 .map_err(|_
| format_err
!("data must be valid UTF-8"))?
;
689 let _lock
= plugin
::lock()?
;
691 let (mut plugins
, expected_digest
) = plugin
::config()?
;
693 if let Some(digest
) = digest
{
694 let digest
= proxmox
::tools
::hex_to_digest(&digest
)?
;
695 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
698 match plugins
.get_mut(&id
) {
699 Some((ty
, ref mut entry
)) => {
701 bail
!("cannot update plugin of type {:?}", ty
);
704 let mut plugin
: DnsPlugin
= serde_json
::from_value(entry
.clone())?
;
706 if let Some(delete
) = delete
{
707 for delete_prop
in delete
{
709 DeletableProperty
::validation_delay
=> { plugin.core.validation_delay = None; }
,
710 DeletableProperty
::disable
=> { plugin.core.disable = None; }
,
714 if let Some(data
) = data { plugin.data = data; }
715 if let Some(api
) = update
.api { plugin.core.api = api; }
716 if update
.validation_delay
.is_some() { plugin.core.validation_delay = update.validation_delay; }
717 if update
.disable
.is_some() { plugin.core.disable = update.disable; }
720 *entry
= serde_json
::to_value(plugin
)?
;
722 None
=> http_bail
!(NOT_FOUND
, "no such plugin"),
725 plugin
::save_config(&plugins
)?
;