3 use anyhow
::{bail, format_err, Error}
;
4 use serde
::{Deserialize, Serialize}
;
5 use serde_json
::{json, Value}
;
7 use proxmox
::api
::router
::SubdirMap
;
8 use proxmox
::api
::schema
::Updatable
;
9 use proxmox
::api
::{api, Permission, Router, RpcEnvironment}
;
10 use proxmox
::http_bail
;
11 use proxmox
::list_subdirs_api_method
;
13 use proxmox_acme_rs
::account
::AccountData
as AcmeAccountData
;
14 use proxmox_acme_rs
::Account
;
16 use crate::acme
::AcmeClient
;
17 use crate::api2
::types
::Authid
;
18 use crate::config
::acl
::PRIV_SYS_MODIFY
;
19 use crate::config
::acme
::plugin
::{
20 DnsPlugin
, DnsPluginCore
, DnsPluginCoreUpdater
, PLUGIN_ID_SCHEMA
,
22 use crate::config
::acme
::{AccountName, KnownAcmeDirectory}
;
23 use crate::server
::WorkerTask
;
24 use crate::tools
::ControlFlow
;
26 pub(crate) const ROUTER
: Router
= Router
::new()
27 .get(&list_subdirs_api_method
!(SUBDIRS
))
30 const SUBDIRS
: SubdirMap
= &[
34 .get(&API_METHOD_LIST_ACCOUNTS
)
35 .post(&API_METHOD_REGISTER_ACCOUNT
)
36 .match_all("name", &ACCOUNT_ITEM_ROUTER
),
40 &Router
::new().get(&API_METHOD_GET_CHALLENGE_SCHEMA
),
44 &Router
::new().get(&API_METHOD_GET_DIRECTORIES
),
49 .get(&API_METHOD_LIST_PLUGINS
)
50 .post(&API_METHOD_ADD_PLUGIN
)
51 .match_all("id", &PLUGIN_ITEM_ROUTER
),
53 ("tos", &Router
::new().get(&API_METHOD_GET_TOS
)),
56 const ACCOUNT_ITEM_ROUTER
: Router
= Router
::new()
57 .get(&API_METHOD_GET_ACCOUNT
)
58 .put(&API_METHOD_UPDATE_ACCOUNT
)
59 .delete(&API_METHOD_DEACTIVATE_ACCOUNT
);
61 const PLUGIN_ITEM_ROUTER
: Router
= Router
::new()
62 .get(&API_METHOD_GET_PLUGIN
)
63 .put(&API_METHOD_UPDATE_PLUGIN
)
64 .delete(&API_METHOD_DELETE_PLUGIN
);
68 name
: { type: AccountName }
,
71 /// An ACME Account entry.
73 /// Currently only contains a 'name' property.
75 pub struct AccountEntry
{
81 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
85 items
: { type: AccountEntry }
,
86 description
: "List of ACME accounts.",
90 /// List ACME accounts.
91 pub fn list_accounts() -> Result
<Vec
<AccountEntry
>, Error
> {
92 let mut entries
= Vec
::new();
93 crate::config
::acme
::foreach_acme_account(|name
| {
94 entries
.push(AccountEntry { name }
);
95 ControlFlow
::Continue(())
102 account
: { type: Object, properties: {}
, additional_properties
: true },
109 /// ACME Account information.
111 /// This is what we return via the API.
113 pub struct AccountInfo
{
114 /// Raw account data.
115 account
: AcmeAccountData
,
117 /// The ACME directory URL the account was created at.
120 /// The account's own URL within the ACME directory.
123 /// The ToS URL, if the user agreed to one.
124 #[serde(skip_serializing_if = "Option::is_none")]
131 name
: { type: AccountName }
,
135 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
137 returns
: { type: AccountInfo }
,
140 /// Return existing ACME account information.
141 pub async
fn get_account(name
: AccountName
) -> Result
<AccountInfo
, Error
> {
142 let client
= AcmeClient
::load(&name
).await?
;
143 let account
= client
.account()?
;
145 location
: account
.location
.clone(),
146 tos
: client
.tos().map(str::to_owned
),
147 directory
: client
.directory_url().to_owned(),
148 account
: AcmeAccountData
{
149 only_return_existing
: false, // don't actually write this out in case it's set
150 ..account
.data
.clone()
155 fn account_contact_from_string(s
: &str) -> Vec
<String
> {
156 s
.split(&[' '
, '
;'
, '
,'
, '
\0'
][..])
157 .map(|s
| format
!("mailto:{}", s
))
169 description
: "List of email addresses.",
172 description
: "URL of CA TermsOfService - setting this indicates agreement.",
177 description
: "The ACME Directory.",
183 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
187 /// Register an ACME account.
189 name
: Option
<AccountName
>,
190 // Todo: email & email-list schema
192 tos_url
: Option
<String
>,
193 directory
: Option
<String
>,
194 rpcenv
: &mut dyn RpcEnvironment
,
195 ) -> Result
<String
, Error
> {
196 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
199 .unwrap_or_else(|| unsafe { AccountName::from_string_unchecked("default".to_string()) }
);
201 if Path
::new(&crate::config
::acme
::account_path(&name
)).exists() {
202 http_bail
!(BAD_REQUEST
, "account {:?} already exists", name
);
205 let directory
= directory
.unwrap_or_else(|| {
206 crate::config
::acme
::DEFAULT_ACME_DIRECTORY_ENTRY
216 move |worker
| async
move {
217 let mut client
= AcmeClient
::new(directory
);
219 worker
.log("Registering ACME account...");
222 do_register_account(&mut client
, &name
, tos_url
.is_some(), contact
, None
).await?
;
225 "Registration successful, account URL: {}",
234 pub async
fn do_register_account
<'a
>(
235 client
: &'a
mut AcmeClient
,
239 rsa_bits
: Option
<u32>,
240 ) -> Result
<&'a Account
, Error
> {
241 let contact
= account_contact_from_string(&contact
);
243 .new_account(name
, agree_to_tos
, contact
, rsa_bits
)
250 name
: { type: AccountName }
,
252 description
: "List of email addresses.",
258 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
262 /// Update an ACME account.
263 pub fn update_account(
265 // Todo: email & email-list schema
266 contact
: Option
<String
>,
267 rpcenv
: &mut dyn RpcEnvironment
,
268 ) -> Result
<String
, Error
> {
269 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
276 move |_worker
| async
move {
277 let data
= match contact
{
278 Some(data
) => json
!({
279 "contact": account_contact_from_string(&data
),
284 AcmeClient
::load(&name
).await?
.update_account(&data
).await?
;
294 name
: { type: AccountName }
,
297 "Delete account data even if the server refuses to deactivate the account.",
304 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
308 /// Deactivate an ACME account.
309 pub fn deactivate_account(
312 rpcenv
: &mut dyn RpcEnvironment
,
313 ) -> Result
<String
, Error
> {
314 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
321 move |worker
| async
move {
322 match AcmeClient
::load(&name
)
324 .update_account(&json
!({"status": "deactivated"}
))
328 Err(err
) if !force
=> return Err(err
),
331 "error deactivating account {:?}, proceedeing anyway - {}",
336 crate::config
::acme
::mark_account_deactivated(&name
)?
;
347 description
: "The ACME Directory.",
353 permission
: &Permission
::Anybody
,
358 description
: "The ACME Directory's ToS URL, if any.",
361 /// Get the Terms of Service URL for an ACME directory.
362 async
fn get_tos(directory
: Option
<String
>) -> Result
<Option
<String
>, Error
> {
363 let directory
= directory
.unwrap_or_else(|| {
364 crate::config
::acme
::DEFAULT_ACME_DIRECTORY_ENTRY
368 Ok(AcmeClient
::new(directory
)
369 .terms_of_service_url()
376 permission
: &Permission
::Anybody
,
379 description
: "List of known ACME directories.",
381 items
: { type: KnownAcmeDirectory }
,
384 /// Get named known ACME directory endpoints.
385 fn get_directories() -> Result
<&'
static [KnownAcmeDirectory
], Error
> {
386 Ok(crate::config
::acme
::KNOWN_ACME_DIRECTORIES
)
393 additional_properties
: true,
402 /// Schema for an ACME challenge plugin.
403 pub struct ChallengeSchema
{
407 /// Human readable name, falls back to id.
411 #[serde(rename = "type")]
414 /// The plugin's parameter schema.
420 permission
: &Permission
::Anybody
,
423 description
: "ACME Challenge Plugin Shema.",
425 items
: { type: ChallengeSchema }
,
428 /// Get named known ACME directory endpoints.
429 fn get_challenge_schema() -> Result
<Vec
<ChallengeSchema
>, Error
> {
430 let mut out
= Vec
::new();
431 crate::config
::acme
::foreach_dns_plugin(|id
| {
432 out
.push(ChallengeSchema
{
436 schema
: Value
::Object(Default
::default()),
438 ControlFlow
::Continue(())
444 #[derive(Default, Deserialize, Serialize)]
445 #[serde(rename_all = "kebab-case")]
446 /// The API's format is inherited from PVE/PMG:
447 pub struct PluginConfig
{
452 #[serde(rename = "type")]
458 /// Plugin configuration data.
459 data
: Option
<String
>,
461 /// Extra delay in seconds to wait before requesting validation.
463 /// Allows to cope with long TTL of DNS records.
464 #[serde(skip_serializing_if = "Option::is_none", default)]
465 validation_delay
: Option
<u32>,
467 /// Flag to disable the config.
468 #[serde(skip_serializing_if = "Option::is_none", default)]
469 disable
: Option
<bool
>,
472 // See PMG/PVE's $modify_cfg_for_api sub
473 fn modify_cfg_for_api(id
: &str, ty
: &str, data
: &Value
) -> PluginConfig
{
474 let mut entry
= data
.clone();
476 let obj
= entry
.as_object_mut().unwrap();
478 obj
.insert("plugin".to_string(), Value
::String(id
.to_owned()));
479 obj
.insert("type".to_string(), Value
::String(ty
.to_owned()));
481 // FIXME: This needs to go once the `Updater` is fixed.
482 // None of these should be able to fail unless the user changed the files by hand, in which
483 // case we leave the unmodified string in the Value for now. This will be handled with an error
485 if let Some(Value
::String(ref mut data
)) = obj
.get_mut("data") {
486 if let Ok(new
) = base64
::decode_config(&data
, base64
::URL_SAFE_NO_PAD
) {
487 if let Ok(utf8
) = String
::from_utf8(new
) {
493 // PVE/PMG do this explicitly for ACME plugins...
494 // obj.insert("digest".to_string(), Value::String(digest.clone()));
496 serde_json
::from_value(entry
).unwrap_or_else(|_
| PluginConfig
{
497 plugin
: "*Error*".to_string(),
498 ty
: "*Error*".to_string(),
505 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
510 description
: "List of ACME plugin configurations.",
511 items
: { type: PluginConfig }
,
514 /// List ACME challenge plugins.
515 pub fn list_plugins(mut rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Vec
<PluginConfig
>, Error
> {
516 use crate::config
::acme
::plugin
;
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 use crate::config
::acme
::plugin
;
542 let (plugins
, digest
) = plugin
::config()?
;
543 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
545 match plugins
.get(&id
) {
546 Some((ty
, data
)) => Ok(modify_cfg_for_api(&id
, &ty
, &data
)),
547 None
=> http_bail
!(NOT_FOUND
, "no such plugin"),
551 // Currently we only have "the" standalone plugin and DNS plugins so we can just flatten a
554 // FIXME: The 'id' parameter should not be "optional" in the schema.
560 description
: "The ACME challenge plugin type.",
563 type: DnsPluginCoreUpdater
,
568 // This is different in the API!
569 description
: "DNS plugin data (base64 encoded with padding).",
574 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
578 /// Add ACME plugin configuration.
579 pub fn add_plugin(r
#type: String, core: DnsPluginCoreUpdater, data: String) -> Result<(), Error> {
580 use crate::config
::acme
::plugin
;
582 // Currently we only support DNS plugins and the standalone plugin is "fixed":
584 bail
!("invalid ACME plugin type: {:?}", r
#type);
587 let data
= String
::from_utf8(base64
::decode(&data
)?
)
588 .map_err(|_
| format_err
!("data must be valid UTF-8"))?
;
591 // FIXME: Solve the Updater with non-optional fields thing...
595 .ok_or_else(|| format_err
!("missing required 'id' parameter"))?
;
597 let _lock
= plugin
::lock()?
;
599 let (mut plugins
, _digest
) = plugin
::config()?
;
600 if plugins
.contains_key(&id
) {
601 bail
!("ACME plugin ID {:?} already exists", id
);
604 let plugin
= serde_json
::to_value(DnsPlugin
{
605 core
: DnsPluginCore
::try_build_from(core
)?
,
609 plugins
.insert(id
, r
#type, plugin);
611 plugin
::save_config(&plugins
)?
;
619 id
: { schema: PLUGIN_ID_SCHEMA }
,
623 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
627 /// Delete an ACME plugin configuration.
628 pub fn delete_plugin(id
: String
) -> Result
<(), Error
> {
629 use crate::config
::acme
::plugin
;
631 let _lock
= plugin
::lock()?
;
633 let (mut plugins
, _digest
) = plugin
::config()?
;
634 if plugins
.remove(&id
).is_none() {
635 http_bail
!(NOT_FOUND
, "no such plugin");
637 plugin
::save_config(&plugins
)?
;
646 type: DnsPluginCoreUpdater
,
652 // This is different in the API!
653 description
: "DNS plugin data (base64 encoded with padding).",
656 description
: "Digest to protect against concurrent updates",
660 description
: "Options to remove from the configuration",
666 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
670 /// Update an ACME plugin configuration.
671 pub fn update_plugin(
672 core_update
: DnsPluginCoreUpdater
,
673 data
: Option
<String
>,
674 delete
: Option
<String
>,
675 digest
: Option
<String
>,
676 ) -> Result
<(), Error
> {
677 use crate::config
::acme
::plugin
;
683 .map(String
::from_utf8
)
685 .map_err(|_
| format_err
!("data must be valid UTF-8"))?
;
686 //core_update.api_fixup()?;
688 // unwrap: the id is matched by this method's API path
689 let id
= core_update
.id
.clone().unwrap();
691 let delete
: Vec
<&str> = delete
694 .split(&[' '
, '
,'
, '
;'
, '
\0'
][..])
697 let _lock
= plugin
::lock()?
;
699 let (mut plugins
, expected_digest
) = plugin
::config()?
;
701 if let Some(digest
) = digest
{
702 let digest
= proxmox
::tools
::hex_to_digest(&digest
)?
;
703 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
706 match plugins
.get_mut(&id
) {
707 Some((ty
, ref mut entry
)) => {
709 bail
!("cannot update plugin of type {:?}", ty
);
712 let mut plugin
: DnsPlugin
= serde_json
::from_value(entry
.clone())?
;
713 plugin
.core
.update_from(core_update
, &delete
)?
;
714 if let Some(data
) = data
{
717 *entry
= serde_json
::to_value(plugin
)?
;
719 None
=> http_bail
!(NOT_FOUND
, "no such plugin"),
722 plugin
::save_config(&plugins
)?
;