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}
;
12 http_bail
, list_subdirs_api_method
, Permission
, Router
, SubdirMap
, RpcEnvironment
,
14 use proxmox_schema
::api
;
16 use proxmox_acme_rs
::account
::AccountData
as AcmeAccountData
;
17 use proxmox_acme_rs
::Account
;
19 use pbs_api_types
::{Authid, PRIV_SYS_MODIFY}
;
20 use pbs_tools
::ops
::ControlFlow
;
21 use pbs_tools
::{task_log, task_warn}
;
23 use crate::acme
::AcmeClient
;
24 use crate::api2
::types
::{AcmeAccountName, AcmeChallengeSchema, KnownAcmeDirectory}
;
25 use crate::config
::acme
::plugin
::{
26 self, DnsPlugin
, DnsPluginCore
, DnsPluginCoreUpdater
, PLUGIN_ID_SCHEMA
,
28 use proxmox_rest_server
::WorkerTask
;
30 pub(crate) const ROUTER
: Router
= Router
::new()
31 .get(&list_subdirs_api_method
!(SUBDIRS
))
34 const SUBDIRS
: SubdirMap
= &[
38 .get(&API_METHOD_LIST_ACCOUNTS
)
39 .post(&API_METHOD_REGISTER_ACCOUNT
)
40 .match_all("name", &ACCOUNT_ITEM_ROUTER
),
44 &Router
::new().get(&API_METHOD_GET_CHALLENGE_SCHEMA
),
48 &Router
::new().get(&API_METHOD_GET_DIRECTORIES
),
53 .get(&API_METHOD_LIST_PLUGINS
)
54 .post(&API_METHOD_ADD_PLUGIN
)
55 .match_all("id", &PLUGIN_ITEM_ROUTER
),
57 ("tos", &Router
::new().get(&API_METHOD_GET_TOS
)),
60 const ACCOUNT_ITEM_ROUTER
: Router
= Router
::new()
61 .get(&API_METHOD_GET_ACCOUNT
)
62 .put(&API_METHOD_UPDATE_ACCOUNT
)
63 .delete(&API_METHOD_DEACTIVATE_ACCOUNT
);
65 const PLUGIN_ITEM_ROUTER
: Router
= Router
::new()
66 .get(&API_METHOD_GET_PLUGIN
)
67 .put(&API_METHOD_UPDATE_PLUGIN
)
68 .delete(&API_METHOD_DELETE_PLUGIN
);
72 name
: { type: AcmeAccountName }
,
75 /// An ACME Account entry.
77 /// Currently only contains a 'name' property.
79 pub struct AccountEntry
{
80 name
: AcmeAccountName
,
85 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
89 items
: { type: AccountEntry }
,
90 description
: "List of ACME accounts.",
94 /// List ACME accounts.
95 pub fn list_accounts() -> Result
<Vec
<AccountEntry
>, Error
> {
96 let mut entries
= Vec
::new();
97 crate::config
::acme
::foreach_acme_account(|name
| {
98 entries
.push(AccountEntry { name }
);
99 ControlFlow
::Continue(())
106 account
: { type: Object, properties: {}
, additional_properties
: true },
113 /// ACME Account information.
115 /// This is what we return via the API.
117 pub struct AccountInfo
{
118 /// Raw account data.
119 account
: AcmeAccountData
,
121 /// The ACME directory URL the account was created at.
124 /// The account's own URL within the ACME directory.
127 /// The ToS URL, if the user agreed to one.
128 #[serde(skip_serializing_if = "Option::is_none")]
135 name
: { type: AcmeAccountName }
,
139 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
141 returns
: { type: AccountInfo }
,
144 /// Return existing ACME account information.
145 pub async
fn get_account(name
: AcmeAccountName
) -> Result
<AccountInfo
, Error
> {
146 let client
= AcmeClient
::load(&name
).await?
;
147 let account
= client
.account()?
;
149 location
: account
.location
.clone(),
150 tos
: client
.tos().map(str::to_owned
),
151 directory
: client
.directory_url().to_owned(),
152 account
: AcmeAccountData
{
153 only_return_existing
: false, // don't actually write this out in case it's set
154 ..account
.data
.clone()
159 fn account_contact_from_string(s
: &str) -> Vec
<String
> {
160 s
.split(&[' '
, '
;'
, '
,'
, '
\0'
][..])
161 .map(|s
| format
!("mailto:{}", s
))
169 type: AcmeAccountName
,
173 description
: "List of email addresses.",
176 description
: "URL of CA TermsOfService - setting this indicates agreement.",
181 description
: "The ACME Directory.",
187 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
191 /// Register an ACME account.
193 name
: Option
<AcmeAccountName
>,
194 // Todo: email & email-list schema
196 tos_url
: Option
<String
>,
197 directory
: Option
<String
>,
198 rpcenv
: &mut dyn RpcEnvironment
,
199 ) -> Result
<String
, Error
> {
200 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
202 let name
= name
.unwrap_or_else(|| unsafe {
203 AcmeAccountName
::from_string_unchecked("default".to_string())
206 if Path
::new(&crate::config
::acme
::account_path(&name
)).exists() {
207 http_bail
!(BAD_REQUEST
, "account {} already exists", name
);
210 let directory
= directory
.unwrap_or_else(|| {
211 crate::config
::acme
::DEFAULT_ACME_DIRECTORY_ENTRY
218 Some(name
.to_string()),
221 move |worker
| async
move {
222 let mut client
= AcmeClient
::new(directory
);
224 task_log
!(worker
, "Registering ACME account '{}'...", &name
);
227 do_register_account(&mut client
, &name
, tos_url
.is_some(), contact
, None
).await?
;
231 "Registration successful, account URL: {}",
240 pub async
fn do_register_account
<'a
>(
241 client
: &'a
mut AcmeClient
,
242 name
: &AcmeAccountName
,
245 rsa_bits
: Option
<u32>,
246 ) -> Result
<&'a Account
, Error
> {
247 let contact
= account_contact_from_string(&contact
);
249 .new_account(name
, agree_to_tos
, contact
, rsa_bits
)
256 name
: { type: AcmeAccountName }
,
258 description
: "List of email addresses.",
264 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
268 /// Update an ACME account.
269 pub fn update_account(
270 name
: AcmeAccountName
,
271 // Todo: email & email-list schema
272 contact
: Option
<String
>,
273 rpcenv
: &mut dyn RpcEnvironment
,
274 ) -> Result
<String
, Error
> {
275 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
279 Some(name
.to_string()),
282 move |_worker
| async
move {
283 let data
= match contact
{
284 Some(data
) => json
!({
285 "contact": account_contact_from_string(&data
),
290 AcmeClient
::load(&name
).await?
.update_account(&data
).await?
;
300 name
: { type: AcmeAccountName }
,
303 "Delete account data even if the server refuses to deactivate the account.",
310 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
314 /// Deactivate an ACME account.
315 pub fn deactivate_account(
316 name
: AcmeAccountName
,
318 rpcenv
: &mut dyn RpcEnvironment
,
319 ) -> Result
<String
, Error
> {
320 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
324 Some(name
.to_string()),
327 move |worker
| async
move {
328 match AcmeClient
::load(&name
)
330 .update_account(&json
!({"status": "deactivated"}
))
334 Err(err
) if !force
=> return Err(err
),
338 "error deactivating account {}, proceedeing anyway - {}",
343 crate::config
::acme
::mark_account_deactivated(&name
)?
;
354 description
: "The ACME Directory.",
360 permission
: &Permission
::Anybody
,
365 description
: "The ACME Directory's ToS URL, if any.",
368 /// Get the Terms of Service URL for an ACME directory.
369 async
fn get_tos(directory
: Option
<String
>) -> Result
<Option
<String
>, Error
> {
370 let directory
= directory
.unwrap_or_else(|| {
371 crate::config
::acme
::DEFAULT_ACME_DIRECTORY_ENTRY
375 Ok(AcmeClient
::new(directory
)
376 .terms_of_service_url()
383 permission
: &Permission
::Anybody
,
386 description
: "List of known ACME directories.",
388 items
: { type: KnownAcmeDirectory }
,
391 /// Get named known ACME directory endpoints.
392 fn get_directories() -> Result
<&'
static [KnownAcmeDirectory
], Error
> {
393 Ok(crate::config
::acme
::KNOWN_ACME_DIRECTORIES
)
396 /// Wrapper for efficient Arc use when returning the ACME challenge-plugin schema for serializing
397 struct ChallengeSchemaWrapper
{
398 inner
: Arc
<Vec
<AcmeChallengeSchema
>>,
401 impl Serialize
for ChallengeSchemaWrapper
{
402 fn serialize
<S
>(&self, serializer
: S
) -> Result
<S
::Ok
, S
::Error
>
404 S
: serde
::Serializer
,
406 self.inner
.serialize(serializer
)
410 fn get_cached_challenge_schemas() -> Result
<ChallengeSchemaWrapper
, Error
> {
412 static ref CACHE
: Mutex
<Option
<(Arc
<Vec
<AcmeChallengeSchema
>>, SystemTime
)>> =
416 // the actual loading code
417 let mut last
= CACHE
.lock().unwrap();
419 let actual_mtime
= fs
::metadata(crate::config
::acme
::ACME_DNS_SCHEMA_FN
)?
.modified()?
;
421 let schema
= match &*last
{
422 Some((schema
, cached_mtime
)) if *cached_mtime
>= actual_mtime
=> schema
.clone(),
424 let new_schema
= Arc
::new(crate::config
::acme
::load_dns_challenge_schema()?
);
425 *last
= Some((Arc
::clone(&new_schema
), actual_mtime
));
430 Ok(ChallengeSchemaWrapper { inner: schema }
)
435 permission
: &Permission
::Anybody
,
438 description
: "ACME Challenge Plugin Shema.",
440 items
: { type: AcmeChallengeSchema }
,
443 /// Get named known ACME directory endpoints.
444 fn get_challenge_schema() -> Result
<ChallengeSchemaWrapper
, Error
> {
445 get_cached_challenge_schemas()
449 #[derive(Default, Deserialize, Serialize)]
450 #[serde(rename_all = "kebab-case")]
451 /// The API's format is inherited from PVE/PMG:
452 pub struct PluginConfig
{
457 #[serde(rename = "type")]
463 /// Plugin configuration data.
464 data
: Option
<String
>,
466 /// Extra delay in seconds to wait before requesting validation.
468 /// Allows to cope with long TTL of DNS records.
469 #[serde(skip_serializing_if = "Option::is_none", default)]
470 alidation_delay
: Option
<u32>,
472 /// Flag to disable the config.
473 #[serde(skip_serializing_if = "Option::is_none", default)]
474 disable
: Option
<bool
>,
477 // See PMG/PVE's $modify_cfg_for_api sub
478 fn modify_cfg_for_api(id
: &str, ty
: &str, data
: &Value
) -> PluginConfig
{
479 let mut entry
= data
.clone();
481 let obj
= entry
.as_object_mut().unwrap();
483 obj
.insert("plugin".to_string(), Value
::String(id
.to_owned()));
484 obj
.insert("type".to_string(), Value
::String(ty
.to_owned()));
486 // FIXME: This needs to go once the `Updater` is fixed.
487 // None of these should be able to fail unless the user changed the files by hand, in which
488 // case we leave the unmodified string in the Value for now. This will be handled with an error
490 if let Some(Value
::String(ref mut data
)) = obj
.get_mut("data") {
491 if let Ok(new
) = base64
::decode_config(&data
, base64
::URL_SAFE_NO_PAD
) {
492 if let Ok(utf8
) = String
::from_utf8(new
) {
498 // PVE/PMG do this explicitly for ACME plugins...
499 // obj.insert("digest".to_string(), Value::String(digest.clone()));
501 serde_json
::from_value(entry
).unwrap_or_else(|_
| PluginConfig
{
502 plugin
: "*Error*".to_string(),
503 ty
: "*Error*".to_string(),
510 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
515 description
: "List of ACME plugin configurations.",
516 items
: { type: PluginConfig }
,
519 /// List ACME challenge plugins.
520 pub fn list_plugins(mut rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Vec
<PluginConfig
>, Error
> {
521 let (plugins
, digest
) = plugin
::config()?
;
522 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
525 .map(|(id
, (ty
, data
))| modify_cfg_for_api(&id
, &ty
, data
))
532 id
: { schema: PLUGIN_ID_SCHEMA }
,
536 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
539 returns
: { type: PluginConfig }
,
541 /// List ACME challenge plugins.
542 pub fn get_plugin(id
: String
, mut rpcenv
: &mut dyn RpcEnvironment
) -> Result
<PluginConfig
, Error
> {
543 let (plugins
, digest
) = plugin
::config()?
;
544 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
546 match plugins
.get(&id
) {
547 Some((ty
, data
)) => Ok(modify_cfg_for_api(&id
, &ty
, &data
)),
548 None
=> http_bail
!(NOT_FOUND
, "no such plugin"),
552 // Currently we only have "the" standalone plugin and DNS plugins so we can just flatten a
555 // FIXME: The 'id' parameter should not be "optional" in the schema.
561 description
: "The ACME challenge plugin type.",
569 // This is different in the API!
570 description
: "DNS plugin data (base64 encoded with padding).",
575 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
579 /// Add ACME plugin configuration.
580 pub fn add_plugin(r
#type: String, core: DnsPluginCore, data: String) -> Result<(), Error> {
581 // Currently we only support DNS plugins and the standalone plugin is "fixed":
583 bail
!("invalid ACME plugin type: {:?}", r
#type);
586 let data
= String
::from_utf8(base64
::decode(&data
)?
)
587 .map_err(|_
| format_err
!("data must be valid UTF-8"))?
;
589 let id
= core
.id
.clone();
591 let _lock
= plugin
::lock()?
;
593 let (mut plugins
, _digest
) = plugin
::config()?
;
594 if plugins
.contains_key(&id
) {
595 bail
!("ACME plugin ID {:?} already exists", id
);
598 let plugin
= serde_json
::to_value(DnsPlugin { core, data }
)?
;
600 plugins
.insert(id
, r
#type, plugin);
602 plugin
::save_config(&plugins
)?
;
610 id
: { schema: PLUGIN_ID_SCHEMA }
,
614 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
618 /// Delete an ACME plugin configuration.
619 pub fn delete_plugin(id
: String
) -> Result
<(), Error
> {
620 let _lock
= plugin
::lock()?
;
622 let (mut plugins
, _digest
) = plugin
::config()?
;
623 if plugins
.remove(&id
).is_none() {
624 http_bail
!(NOT_FOUND
, "no such plugin");
626 plugin
::save_config(&plugins
)?
;
632 #[derive(Serialize, Deserialize)]
633 #[serde(rename_all="kebab-case")]
634 #[allow(non_camel_case_types)]
635 /// Deletable property name
636 pub enum DeletableProperty
{
637 /// Delete the disable property
639 /// Delete the validation-delay property
646 id
: { schema: PLUGIN_ID_SCHEMA }
,
648 type: DnsPluginCoreUpdater
,
654 // This is different in the API!
655 description
: "DNS plugin data (base64 encoded with padding).",
658 description
: "List of properties to delete.",
662 type: DeletableProperty
,
666 description
: "Digest to protect against concurrent updates",
672 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
676 /// Update an ACME plugin configuration.
677 pub fn update_plugin(
679 update
: DnsPluginCoreUpdater
,
680 data
: Option
<String
>,
681 delete
: Option
<Vec
<DeletableProperty
>>,
682 digest
: Option
<String
>,
683 ) -> Result
<(), Error
> {
688 .map(String
::from_utf8
)
690 .map_err(|_
| format_err
!("data must be valid UTF-8"))?
;
692 let _lock
= plugin
::lock()?
;
694 let (mut plugins
, expected_digest
) = plugin
::config()?
;
696 if let Some(digest
) = digest
{
697 let digest
= proxmox
::tools
::hex_to_digest(&digest
)?
;
698 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
701 match plugins
.get_mut(&id
) {
702 Some((ty
, ref mut entry
)) => {
704 bail
!("cannot update plugin of type {:?}", ty
);
707 let mut plugin
: DnsPlugin
= serde_json
::from_value(entry
.clone())?
;
709 if let Some(delete
) = delete
{
710 for delete_prop
in delete
{
712 DeletableProperty
::validation_delay
=> { plugin.core.validation_delay = None; }
,
713 DeletableProperty
::disable
=> { plugin.core.disable = None; }
,
717 if let Some(data
) = data { plugin.data = data; }
718 if let Some(api
) = update
.api { plugin.core.api = api; }
719 if update
.validation_delay
.is_some() { plugin.core.validation_delay = update.validation_delay; }
720 if update
.disable
.is_some() { plugin.core.disable = update.disable; }
723 *entry
= serde_json
::to_value(plugin
)?
;
725 None
=> http_bail
!(NOT_FOUND
, "no such plugin"),
728 plugin
::save_config(&plugins
)?
;