2 use std
::ops
::ControlFlow
;
4 use std
::sync
::{Arc, Mutex}
;
5 use std
::time
::SystemTime
;
7 use anyhow
::{bail, format_err, Error}
;
8 use lazy_static
::lazy_static
;
9 use serde
::{Deserialize, Serialize}
;
10 use serde_json
::{json, Value}
;
14 http_bail
, list_subdirs_api_method
, Permission
, Router
, RpcEnvironment
, SubdirMap
,
16 use proxmox_schema
::api
;
17 use proxmox_sys
::{task_log, task_warn}
;
19 use proxmox_acme_rs
::account
::AccountData
as AcmeAccountData
;
20 use proxmox_acme_rs
::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.",
188 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
192 /// Register an ACME account.
194 name
: Option
<AcmeAccountName
>,
195 // Todo: email & email-list schema
197 tos_url
: Option
<String
>,
198 directory
: Option
<String
>,
199 rpcenv
: &mut dyn RpcEnvironment
,
200 ) -> Result
<String
, Error
> {
201 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
203 let name
= name
.unwrap_or_else(|| unsafe {
204 AcmeAccountName
::from_string_unchecked("default".to_string())
207 if Path
::new(&crate::config
::acme
::account_path(&name
)).exists() {
208 http_bail
!(BAD_REQUEST
, "account {} already exists", name
);
211 let directory
= directory
.unwrap_or_else(|| {
212 crate::config
::acme
::DEFAULT_ACME_DIRECTORY_ENTRY
219 Some(name
.to_string()),
222 move |worker
| async
move {
223 let mut client
= AcmeClient
::new(directory
);
225 task_log
!(worker
, "Registering ACME account '{}'...", &name
);
228 do_register_account(&mut client
, &name
, tos_url
.is_some(), contact
, None
).await?
;
232 "Registration successful, account URL: {}",
241 pub async
fn do_register_account
<'a
>(
242 client
: &'a
mut AcmeClient
,
243 name
: &AcmeAccountName
,
246 rsa_bits
: Option
<u32>,
247 ) -> Result
<&'a Account
, Error
> {
248 let contact
= account_contact_from_string(&contact
);
250 .new_account(name
, agree_to_tos
, contact
, rsa_bits
)
257 name
: { type: AcmeAccountName }
,
259 description
: "List of email addresses.",
265 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
269 /// Update an ACME account.
270 pub fn update_account(
271 name
: AcmeAccountName
,
272 // Todo: email & email-list schema
273 contact
: Option
<String
>,
274 rpcenv
: &mut dyn RpcEnvironment
,
275 ) -> Result
<String
, Error
> {
276 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
280 Some(name
.to_string()),
283 move |_worker
| async
move {
284 let data
= match contact
{
285 Some(data
) => json
!({
286 "contact": account_contact_from_string(&data
),
291 AcmeClient
::load(&name
).await?
.update_account(&data
).await?
;
301 name
: { type: AcmeAccountName }
,
304 "Delete account data even if the server refuses to deactivate the account.",
311 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
315 /// Deactivate an ACME account.
316 pub fn deactivate_account(
317 name
: AcmeAccountName
,
319 rpcenv
: &mut dyn RpcEnvironment
,
320 ) -> Result
<String
, Error
> {
321 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
325 Some(name
.to_string()),
328 move |worker
| async
move {
329 match AcmeClient
::load(&name
)
331 .update_account(&json
!({"status": "deactivated"}
))
335 Err(err
) if !force
=> return Err(err
),
339 "error deactivating account {}, proceedeing anyway - {}",
345 crate::config
::acme
::mark_account_deactivated(&name
)?
;
356 description
: "The ACME Directory.",
362 permission
: &Permission
::Anybody
,
367 description
: "The ACME Directory's ToS URL, if any.",
370 /// Get the Terms of Service URL for an ACME directory.
371 async
fn get_tos(directory
: Option
<String
>) -> Result
<Option
<String
>, Error
> {
372 let directory
= directory
.unwrap_or_else(|| {
373 crate::config
::acme
::DEFAULT_ACME_DIRECTORY_ENTRY
377 Ok(AcmeClient
::new(directory
)
378 .terms_of_service_url()
385 permission
: &Permission
::Anybody
,
388 description
: "List of known ACME directories.",
390 items
: { type: KnownAcmeDirectory }
,
393 /// Get named known ACME directory endpoints.
394 fn get_directories() -> Result
<&'
static [KnownAcmeDirectory
], Error
> {
395 Ok(crate::config
::acme
::KNOWN_ACME_DIRECTORIES
)
398 /// Wrapper for efficient Arc use when returning the ACME challenge-plugin schema for serializing
399 struct ChallengeSchemaWrapper
{
400 inner
: Arc
<Vec
<AcmeChallengeSchema
>>,
403 impl Serialize
for ChallengeSchemaWrapper
{
404 fn serialize
<S
>(&self, serializer
: S
) -> Result
<S
::Ok
, S
::Error
>
406 S
: serde
::Serializer
,
408 self.inner
.serialize(serializer
)
412 fn get_cached_challenge_schemas() -> Result
<ChallengeSchemaWrapper
, Error
> {
414 static ref CACHE
: Mutex
<Option
<(Arc
<Vec
<AcmeChallengeSchema
>>, SystemTime
)>> =
418 // the actual loading code
419 let mut last
= CACHE
.lock().unwrap();
421 let actual_mtime
= fs
::metadata(crate::config
::acme
::ACME_DNS_SCHEMA_FN
)?
.modified()?
;
423 let schema
= match &*last
{
424 Some((schema
, cached_mtime
)) if *cached_mtime
>= actual_mtime
=> schema
.clone(),
426 let new_schema
= Arc
::new(crate::config
::acme
::load_dns_challenge_schema()?
);
427 *last
= Some((Arc
::clone(&new_schema
), actual_mtime
));
432 Ok(ChallengeSchemaWrapper { inner: schema }
)
437 permission
: &Permission
::Anybody
,
440 description
: "ACME Challenge Plugin Shema.",
442 items
: { type: AcmeChallengeSchema }
,
445 /// Get named known ACME directory endpoints.
446 fn get_challenge_schema() -> Result
<ChallengeSchemaWrapper
, Error
> {
447 get_cached_challenge_schemas()
451 #[derive(Default, Deserialize, Serialize)]
452 #[serde(rename_all = "kebab-case")]
453 /// The API's format is inherited from PVE/PMG:
454 pub struct PluginConfig
{
459 #[serde(rename = "type")]
465 /// Plugin configuration data.
466 data
: Option
<String
>,
468 /// Extra delay in seconds to wait before requesting validation.
470 /// Allows to cope with long TTL of DNS records.
471 #[serde(skip_serializing_if = "Option::is_none", default)]
472 validation_delay
: Option
<u32>,
474 /// Flag to disable the config.
475 #[serde(skip_serializing_if = "Option::is_none", default)]
476 disable
: Option
<bool
>,
479 // See PMG/PVE's $modify_cfg_for_api sub
480 fn modify_cfg_for_api(id
: &str, ty
: &str, data
: &Value
) -> PluginConfig
{
481 let mut entry
= data
.clone();
483 let obj
= entry
.as_object_mut().unwrap();
485 obj
.insert("plugin".to_string(), Value
::String(id
.to_owned()));
486 obj
.insert("type".to_string(), Value
::String(ty
.to_owned()));
488 // FIXME: This needs to go once the `Updater` is fixed.
489 // None of these should be able to fail unless the user changed the files by hand, in which
490 // case we leave the unmodified string in the Value for now. This will be handled with an error
492 if let Some(Value
::String(ref mut data
)) = obj
.get_mut("data") {
493 if let Ok(new
) = base64
::decode_config(&data
, base64
::URL_SAFE_NO_PAD
) {
494 if let Ok(utf8
) = String
::from_utf8(new
) {
500 // PVE/PMG do this explicitly for ACME plugins...
501 // obj.insert("digest".to_string(), Value::String(digest.clone()));
503 serde_json
::from_value(entry
).unwrap_or_else(|_
| PluginConfig
{
504 plugin
: "*Error*".to_string(),
505 ty
: "*Error*".to_string(),
512 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
517 description
: "List of ACME plugin configurations.",
518 items
: { type: PluginConfig }
,
521 /// List ACME challenge plugins.
522 pub fn list_plugins(mut rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Vec
<PluginConfig
>, Error
> {
523 let (plugins
, digest
) = plugin
::config()?
;
524 rpcenv
["digest"] = hex
::encode(&digest
).into();
527 .map(|(id
, (ty
, data
))| modify_cfg_for_api(id
, ty
, data
))
534 id
: { schema: PLUGIN_ID_SCHEMA }
,
538 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
541 returns
: { type: PluginConfig }
,
543 /// List ACME challenge plugins.
544 pub fn get_plugin(id
: String
, mut rpcenv
: &mut dyn RpcEnvironment
) -> Result
<PluginConfig
, Error
> {
545 let (plugins
, digest
) = plugin
::config()?
;
546 rpcenv
["digest"] = hex
::encode(&digest
).into();
548 match plugins
.get(&id
) {
549 Some((ty
, data
)) => Ok(modify_cfg_for_api(&id
, ty
, data
)),
550 None
=> http_bail
!(NOT_FOUND
, "no such plugin"),
554 // Currently we only have "the" standalone plugin and DNS plugins so we can just flatten a
557 // FIXME: The 'id' parameter should not be "optional" in the schema.
563 description
: "The ACME challenge plugin type.",
571 // This is different in the API!
572 description
: "DNS plugin data (base64 encoded with padding).",
577 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
581 /// Add ACME plugin configuration.
582 pub fn add_plugin(r
#type: String, core: DnsPluginCore, data: String) -> Result<(), Error> {
583 // Currently we only support DNS plugins and the standalone plugin is "fixed":
585 bail
!("invalid ACME plugin type: {:?}", r
#type);
588 let data
= String
::from_utf8(base64
::decode(&data
)?
)
589 .map_err(|_
| format_err
!("data must be valid UTF-8"))?
;
591 let id
= core
.id
.clone();
593 let _lock
= plugin
::lock()?
;
595 let (mut plugins
, _digest
) = plugin
::config()?
;
596 if plugins
.contains_key(&id
) {
597 bail
!("ACME plugin ID {:?} already exists", id
);
600 let plugin
= serde_json
::to_value(DnsPlugin { core, data }
)?
;
602 plugins
.insert(id
, r
#type, plugin);
604 plugin
::save_config(&plugins
)?
;
612 id
: { schema: PLUGIN_ID_SCHEMA }
,
616 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
620 /// Delete an ACME plugin configuration.
621 pub fn delete_plugin(id
: String
) -> Result
<(), Error
> {
622 let _lock
= plugin
::lock()?
;
624 let (mut plugins
, _digest
) = plugin
::config()?
;
625 if plugins
.remove(&id
).is_none() {
626 http_bail
!(NOT_FOUND
, "no such plugin");
628 plugin
::save_config(&plugins
)?
;
634 #[derive(Serialize, Deserialize)]
635 #[serde(rename_all = "kebab-case")]
636 #[allow(non_camel_case_types)]
637 /// Deletable property name
638 pub enum DeletableProperty
{
639 /// Delete the disable property
641 /// Delete the validation-delay property
648 id
: { schema: PLUGIN_ID_SCHEMA }
,
650 type: DnsPluginCoreUpdater
,
656 // This is different in the API!
657 description
: "DNS plugin data (base64 encoded with padding).",
660 description
: "List of properties to delete.",
664 type: DeletableProperty
,
668 description
: "Digest to protect against concurrent updates",
674 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
678 /// Update an ACME plugin configuration.
679 pub fn update_plugin(
681 update
: DnsPluginCoreUpdater
,
682 data
: Option
<String
>,
683 delete
: Option
<Vec
<DeletableProperty
>>,
684 digest
: Option
<String
>,
685 ) -> Result
<(), Error
> {
690 .map(String
::from_utf8
)
692 .map_err(|_
| format_err
!("data must be valid UTF-8"))?
;
694 let _lock
= plugin
::lock()?
;
696 let (mut plugins
, expected_digest
) = plugin
::config()?
;
698 if let Some(digest
) = digest
{
699 let digest
= <[u8; 32]>::from_hex(&digest
)?
;
700 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
703 match plugins
.get_mut(&id
) {
704 Some((ty
, ref mut entry
)) => {
706 bail
!("cannot update plugin of type {:?}", ty
);
709 let mut plugin
: DnsPlugin
= serde_json
::from_value(entry
.clone())?
;
711 if let Some(delete
) = delete
{
712 for delete_prop
in delete
{
714 DeletableProperty
::validation_delay
=> {
715 plugin
.core
.validation_delay
= None
;
717 DeletableProperty
::disable
=> {
718 plugin
.core
.disable
= None
;
723 if let Some(data
) = data
{
726 if let Some(api
) = update
.api
{
727 plugin
.core
.api
= api
;
729 if update
.validation_delay
.is_some() {
730 plugin
.core
.validation_delay
= update
.validation_delay
;
732 if update
.disable
.is_some() {
733 plugin
.core
.disable
= update
.disable
;
736 *entry
= serde_json
::to_value(plugin
)?
;
738 None
=> http_bail
!(NOT_FOUND
, "no such plugin"),
741 plugin
::save_config(&plugins
)?
;