2 use std
::ops
::ControlFlow
;
4 use std
::sync
::{Arc, Mutex}
;
5 use std
::time
::SystemTime
;
7 use anyhow
::{bail, format_err, Error}
;
9 use lazy_static
::lazy_static
;
10 use serde
::{Deserialize, Serialize}
;
11 use serde_json
::{json, Value}
;
14 http_bail
, list_subdirs_api_method
, Permission
, Router
, RpcEnvironment
, SubdirMap
,
16 use proxmox_schema
::{api, param_bail}
;
17 use proxmox_sys
::{task_log, task_warn}
;
19 use proxmox_acme
::account
::AccountData
as AcmeAccountData
;
20 use proxmox_acme
::Account
;
22 use pbs_api_types
::{Authid, PRIV_SYS_MODIFY}
;
24 use crate::acme
::AcmeClient
;
25 use crate::api2
::types
::{AcmeAccountName, AcmeChallengeSchema, KnownAcmeDirectory}
;
26 use crate::config
::acme
::plugin
::{
27 self, DnsPlugin
, DnsPluginCore
, DnsPluginCoreUpdater
, PLUGIN_ID_SCHEMA
,
29 use proxmox_rest_server
::WorkerTask
;
31 pub(crate) const ROUTER
: Router
= Router
::new()
32 .get(&list_subdirs_api_method
!(SUBDIRS
))
35 const SUBDIRS
: SubdirMap
= &[
39 .get(&API_METHOD_LIST_ACCOUNTS
)
40 .post(&API_METHOD_REGISTER_ACCOUNT
)
41 .match_all("name", &ACCOUNT_ITEM_ROUTER
),
45 &Router
::new().get(&API_METHOD_GET_CHALLENGE_SCHEMA
),
49 &Router
::new().get(&API_METHOD_GET_DIRECTORIES
),
54 .get(&API_METHOD_LIST_PLUGINS
)
55 .post(&API_METHOD_ADD_PLUGIN
)
56 .match_all("id", &PLUGIN_ITEM_ROUTER
),
58 ("tos", &Router
::new().get(&API_METHOD_GET_TOS
)),
61 const ACCOUNT_ITEM_ROUTER
: Router
= Router
::new()
62 .get(&API_METHOD_GET_ACCOUNT
)
63 .put(&API_METHOD_UPDATE_ACCOUNT
)
64 .delete(&API_METHOD_DEACTIVATE_ACCOUNT
);
66 const PLUGIN_ITEM_ROUTER
: Router
= Router
::new()
67 .get(&API_METHOD_GET_PLUGIN
)
68 .put(&API_METHOD_UPDATE_PLUGIN
)
69 .delete(&API_METHOD_DELETE_PLUGIN
);
73 name
: { type: AcmeAccountName }
,
76 /// An ACME Account entry.
78 /// Currently only contains a 'name' property.
80 pub struct AccountEntry
{
81 name
: AcmeAccountName
,
86 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
90 items
: { type: AccountEntry }
,
91 description
: "List of ACME accounts.",
95 /// List ACME accounts.
96 pub fn list_accounts() -> Result
<Vec
<AccountEntry
>, Error
> {
97 let mut entries
= Vec
::new();
98 crate::config
::acme
::foreach_acme_account(|name
| {
99 entries
.push(AccountEntry { name }
);
100 ControlFlow
::Continue(())
107 account
: { type: Object, properties: {}
, additional_properties
: true },
114 /// ACME Account information.
116 /// This is what we return via the API.
118 pub struct AccountInfo
{
119 /// Raw account data.
120 account
: AcmeAccountData
,
122 /// The ACME directory URL the account was created at.
125 /// The account's own URL within the ACME directory.
128 /// The ToS URL, if the user agreed to one.
129 #[serde(skip_serializing_if = "Option::is_none")]
136 name
: { type: AcmeAccountName }
,
140 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
142 returns
: { type: AccountInfo }
,
145 /// Return existing ACME account information.
146 pub async
fn get_account(name
: AcmeAccountName
) -> Result
<AccountInfo
, Error
> {
147 let client
= AcmeClient
::load(&name
).await?
;
148 let account
= client
.account()?
;
150 location
: account
.location
.clone(),
151 tos
: client
.tos().map(str::to_owned
),
152 directory
: client
.directory_url().to_owned(),
153 account
: AcmeAccountData
{
154 only_return_existing
: false, // don't actually write this out in case it's set
155 ..account
.data
.clone()
160 fn account_contact_from_string(s
: &str) -> Vec
<String
> {
161 s
.split(&[' '
, '
;'
, '
,'
, '
\0'
][..])
162 .map(|s
| format
!("mailto:{}", s
))
170 type: AcmeAccountName
,
174 description
: "List of email addresses.",
177 description
: "URL of CA TermsOfService - setting this indicates agreement.",
182 description
: "The ACME Directory.",
187 description
: "Key Identifier for External Account Binding.",
192 description
: "HMAC Key for External Account Binding.",
198 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
202 /// Register an ACME account.
204 name
: Option
<AcmeAccountName
>,
205 // Todo: email & email-list schema
207 tos_url
: Option
<String
>,
208 directory
: Option
<String
>,
209 eab_kid
: Option
<String
>,
210 eab_hmac_key
: Option
<String
>,
211 rpcenv
: &mut dyn RpcEnvironment
,
212 ) -> Result
<String
, Error
> {
213 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
215 let name
= name
.unwrap_or_else(|| unsafe {
216 AcmeAccountName
::from_string_unchecked("default".to_string())
219 // TODO: this should be done via the api definition, but
220 // the api schema currently lacks this ability (2023-11-06)
221 if eab_kid
.is_some() != eab_hmac_key
.is_some() {
224 "either both or none of 'eab_kid' and 'eab_hmac_key' have to be set."
228 if Path
::new(&crate::config
::acme
::account_path(&name
)).exists() {
229 http_bail
!(BAD_REQUEST
, "account {} already exists", name
);
232 let directory
= directory
.unwrap_or_else(|| {
233 crate::config
::acme
::DEFAULT_ACME_DIRECTORY_ENTRY
240 Some(name
.to_string()),
243 move |worker
| async
move {
244 let mut client
= AcmeClient
::new(directory
);
246 task_log
!(worker
, "Registering ACME account '{}'...", &name
);
248 let account
= do_register_account(
254 eab_kid
.zip(eab_hmac_key
),
260 "Registration successful, account URL: {}",
269 pub async
fn do_register_account
<'a
>(
270 client
: &'a
mut AcmeClient
,
271 name
: &AcmeAccountName
,
274 rsa_bits
: Option
<u32>,
275 eab_creds
: Option
<(String
, String
)>,
276 ) -> Result
<&'a Account
, Error
> {
277 let contact
= account_contact_from_string(&contact
);
279 .new_account(name
, agree_to_tos
, contact
, rsa_bits
, eab_creds
)
286 name
: { type: AcmeAccountName }
,
288 description
: "List of email addresses.",
294 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
298 /// Update an ACME account.
299 pub fn update_account(
300 name
: AcmeAccountName
,
301 // Todo: email & email-list schema
302 contact
: Option
<String
>,
303 rpcenv
: &mut dyn RpcEnvironment
,
304 ) -> Result
<String
, Error
> {
305 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
309 Some(name
.to_string()),
312 move |_worker
| async
move {
313 let data
= match contact
{
314 Some(data
) => json
!({
315 "contact": account_contact_from_string(&data
),
320 AcmeClient
::load(&name
).await?
.update_account(&data
).await?
;
330 name
: { type: AcmeAccountName }
,
333 "Delete account data even if the server refuses to deactivate the account.",
340 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
344 /// Deactivate an ACME account.
345 pub fn deactivate_account(
346 name
: AcmeAccountName
,
348 rpcenv
: &mut dyn RpcEnvironment
,
349 ) -> Result
<String
, Error
> {
350 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
354 Some(name
.to_string()),
357 move |worker
| async
move {
358 match AcmeClient
::load(&name
)
360 .update_account(&json
!({"status": "deactivated"}
))
364 Err(err
) if !force
=> return Err(err
),
368 "error deactivating account {}, proceedeing anyway - {}",
374 crate::config
::acme
::mark_account_deactivated(&name
)?
;
385 description
: "The ACME Directory.",
391 permission
: &Permission
::Anybody
,
396 description
: "The ACME Directory's ToS URL, if any.",
399 /// Get the Terms of Service URL for an ACME directory.
400 async
fn get_tos(directory
: Option
<String
>) -> Result
<Option
<String
>, Error
> {
401 let directory
= directory
.unwrap_or_else(|| {
402 crate::config
::acme
::DEFAULT_ACME_DIRECTORY_ENTRY
406 Ok(AcmeClient
::new(directory
)
407 .terms_of_service_url()
414 permission
: &Permission
::Anybody
,
417 description
: "List of known ACME directories.",
419 items
: { type: KnownAcmeDirectory }
,
422 /// Get named known ACME directory endpoints.
423 fn get_directories() -> Result
<&'
static [KnownAcmeDirectory
], Error
> {
424 Ok(crate::config
::acme
::KNOWN_ACME_DIRECTORIES
)
427 /// Wrapper for efficient Arc use when returning the ACME challenge-plugin schema for serializing
428 struct ChallengeSchemaWrapper
{
429 inner
: Arc
<Vec
<AcmeChallengeSchema
>>,
432 impl Serialize
for ChallengeSchemaWrapper
{
433 fn serialize
<S
>(&self, serializer
: S
) -> Result
<S
::Ok
, S
::Error
>
435 S
: serde
::Serializer
,
437 self.inner
.serialize(serializer
)
441 fn get_cached_challenge_schemas() -> Result
<ChallengeSchemaWrapper
, Error
> {
443 static ref CACHE
: Mutex
<Option
<(Arc
<Vec
<AcmeChallengeSchema
>>, SystemTime
)>> =
447 // the actual loading code
448 let mut last
= CACHE
.lock().unwrap();
450 let actual_mtime
= fs
::metadata(crate::config
::acme
::ACME_DNS_SCHEMA_FN
)?
.modified()?
;
452 let schema
= match &*last
{
453 Some((schema
, cached_mtime
)) if *cached_mtime
>= actual_mtime
=> schema
.clone(),
455 let new_schema
= Arc
::new(crate::config
::acme
::load_dns_challenge_schema()?
);
456 *last
= Some((Arc
::clone(&new_schema
), actual_mtime
));
461 Ok(ChallengeSchemaWrapper { inner: schema }
)
466 permission
: &Permission
::Anybody
,
469 description
: "ACME Challenge Plugin Shema.",
471 items
: { type: AcmeChallengeSchema }
,
474 /// Get named known ACME directory endpoints.
475 fn get_challenge_schema() -> Result
<ChallengeSchemaWrapper
, Error
> {
476 get_cached_challenge_schemas()
480 #[derive(Default, Deserialize, Serialize)]
481 #[serde(rename_all = "kebab-case")]
482 /// The API's format is inherited from PVE/PMG:
483 pub struct PluginConfig
{
488 #[serde(rename = "type")]
492 #[serde(skip_serializing_if = "Option::is_none", default)]
495 /// Plugin configuration data.
496 #[serde(skip_serializing_if = "Option::is_none", default)]
497 data
: Option
<String
>,
499 /// Extra delay in seconds to wait before requesting validation.
501 /// Allows to cope with long TTL of DNS records.
502 #[serde(skip_serializing_if = "Option::is_none", default)]
503 validation_delay
: Option
<u32>,
505 /// Flag to disable the config.
506 #[serde(skip_serializing_if = "Option::is_none", default)]
507 disable
: Option
<bool
>,
510 // See PMG/PVE's $modify_cfg_for_api sub
511 fn modify_cfg_for_api(id
: &str, ty
: &str, data
: &Value
) -> PluginConfig
{
512 let mut entry
= data
.clone();
514 let obj
= entry
.as_object_mut().unwrap();
516 obj
.insert("plugin".to_string(), Value
::String(id
.to_owned()));
517 obj
.insert("type".to_string(), Value
::String(ty
.to_owned()));
519 // FIXME: This needs to go once the `Updater` is fixed.
520 // None of these should be able to fail unless the user changed the files by hand, in which
521 // case we leave the unmodified string in the Value for now. This will be handled with an error
523 if let Some(Value
::String(ref mut data
)) = obj
.get_mut("data") {
524 if let Ok(new
) = base64
::decode_config(&data
, base64
::URL_SAFE_NO_PAD
) {
525 if let Ok(utf8
) = String
::from_utf8(new
) {
531 // PVE/PMG do this explicitly for ACME plugins...
532 // obj.insert("digest".to_string(), Value::String(digest.clone()));
534 serde_json
::from_value(entry
).unwrap_or_else(|_
| PluginConfig
{
535 plugin
: "*Error*".to_string(),
536 ty
: "*Error*".to_string(),
543 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
548 description
: "List of ACME plugin configurations.",
549 items
: { type: PluginConfig }
,
552 /// List ACME challenge plugins.
553 pub fn list_plugins(rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Vec
<PluginConfig
>, Error
> {
554 let (plugins
, digest
) = plugin
::config()?
;
555 rpcenv
["digest"] = hex
::encode(digest
).into();
558 .map(|(id
, (ty
, data
))| modify_cfg_for_api(id
, ty
, data
))
565 id
: { schema: PLUGIN_ID_SCHEMA }
,
569 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
572 returns
: { type: PluginConfig }
,
574 /// List ACME challenge plugins.
575 pub fn get_plugin(id
: String
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<PluginConfig
, Error
> {
576 let (plugins
, digest
) = plugin
::config()?
;
577 rpcenv
["digest"] = hex
::encode(digest
).into();
579 match plugins
.get(&id
) {
580 Some((ty
, data
)) => Ok(modify_cfg_for_api(&id
, ty
, data
)),
581 None
=> http_bail
!(NOT_FOUND
, "no such plugin"),
585 // Currently we only have "the" standalone plugin and DNS plugins so we can just flatten a
588 // FIXME: The 'id' parameter should not be "optional" in the schema.
594 description
: "The ACME challenge plugin type.",
602 // This is different in the API!
603 description
: "DNS plugin data (base64 encoded with padding).",
608 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
612 /// Add ACME plugin configuration.
613 pub fn add_plugin(r
#type: String, core: DnsPluginCore, data: String) -> Result<(), Error> {
614 // Currently we only support DNS plugins and the standalone plugin is "fixed":
616 param_bail
!("type", "invalid ACME plugin type: {:?}", r
#type);
619 let data
= String
::from_utf8(base64
::decode(data
)?
)
620 .map_err(|_
| format_err
!("data must be valid UTF-8"))?
;
622 let id
= core
.id
.clone();
624 let _lock
= plugin
::lock()?
;
626 let (mut plugins
, _digest
) = plugin
::config()?
;
627 if plugins
.contains_key(&id
) {
628 param_bail
!("id", "ACME plugin ID {:?} already exists", id
);
631 let plugin
= serde_json
::to_value(DnsPlugin { core, data }
)?
;
633 plugins
.insert(id
, r
#type, plugin);
635 plugin
::save_config(&plugins
)?
;
643 id
: { schema: PLUGIN_ID_SCHEMA }
,
647 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
651 /// Delete an ACME plugin configuration.
652 pub fn delete_plugin(id
: String
) -> Result
<(), Error
> {
653 let _lock
= plugin
::lock()?
;
655 let (mut plugins
, _digest
) = plugin
::config()?
;
656 if plugins
.remove(&id
).is_none() {
657 http_bail
!(NOT_FOUND
, "no such plugin");
659 plugin
::save_config(&plugins
)?
;
665 #[derive(Serialize, Deserialize)]
666 #[serde(rename_all = "kebab-case")]
667 /// Deletable property name
668 pub enum DeletableProperty
{
669 /// Delete the disable property
671 /// Delete the validation-delay property
678 id
: { schema: PLUGIN_ID_SCHEMA }
,
680 type: DnsPluginCoreUpdater
,
686 // This is different in the API!
687 description
: "DNS plugin data (base64 encoded with padding).",
690 description
: "List of properties to delete.",
694 type: DeletableProperty
,
698 description
: "Digest to protect against concurrent updates",
704 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
708 /// Update an ACME plugin configuration.
709 pub fn update_plugin(
711 update
: DnsPluginCoreUpdater
,
712 data
: Option
<String
>,
713 delete
: Option
<Vec
<DeletableProperty
>>,
714 digest
: Option
<String
>,
715 ) -> Result
<(), Error
> {
720 .map(String
::from_utf8
)
722 .map_err(|_
| format_err
!("data must be valid UTF-8"))?
;
724 let _lock
= plugin
::lock()?
;
726 let (mut plugins
, expected_digest
) = plugin
::config()?
;
728 if let Some(digest
) = digest
{
729 let digest
= <[u8; 32]>::from_hex(digest
)?
;
730 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
733 match plugins
.get_mut(&id
) {
734 Some((ty
, ref mut entry
)) => {
736 bail
!("cannot update plugin of type {:?}", ty
);
739 let mut plugin
= DnsPlugin
::deserialize(&*entry
)?
;
741 if let Some(delete
) = delete
{
742 for delete_prop
in delete
{
744 DeletableProperty
::ValidationDelay
=> {
745 plugin
.core
.validation_delay
= None
;
747 DeletableProperty
::Disable
=> {
748 plugin
.core
.disable
= None
;
753 if let Some(data
) = data
{
756 if let Some(api
) = update
.api
{
757 plugin
.core
.api
= api
;
759 if update
.validation_delay
.is_some() {
760 plugin
.core
.validation_delay
= update
.validation_delay
;
762 if update
.disable
.is_some() {
763 plugin
.core
.disable
= update
.disable
;
766 *entry
= serde_json
::to_value(plugin
)?
;
768 None
=> http_bail
!(NOT_FOUND
, "no such plugin"),
771 plugin
::save_config(&plugins
)?
;