From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id A4FEE73B9E for ; Fri, 16 Apr 2021 15:36:24 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 7F42924FDC for ; Fri, 16 Apr 2021 15:35:35 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 4AED924D2F for ; Fri, 16 Apr 2021 15:35:25 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 0BD8843E58 for ; Fri, 16 Apr 2021 15:35:25 +0200 (CEST) From: Wolfgang Bumiller To: pbs-devel@lists.proxmox.com Date: Fri, 16 Apr 2021 15:35:13 +0200 Message-Id: <20210416133517.23349-21-w.bumiller@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20210416133517.23349-1-w.bumiller@proxmox.com> References: <20210416133517.23349-1-w.bumiller@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.031 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [mod.rs, acme.rs, proxmox-backup-manager.rs] Subject: [pbs-devel] [RFC backup 20/23] add acme commands to proxmox-backup-manager X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 16 Apr 2021 13:36:24 -0000 Signed-off-by: Wolfgang Bumiller --- src/bin/proxmox-backup-manager.rs | 1 + src/bin/proxmox_backup_manager/acme.rs | 414 +++++++++++++++++++++++++ src/bin/proxmox_backup_manager/mod.rs | 2 + 3 files changed, 417 insertions(+) create mode 100644 src/bin/proxmox_backup_manager/acme.rs diff --git a/src/bin/proxmox-backup-manager.rs b/src/bin/proxmox-backup-manager.rs index 105a11f8..522c800e 100644 --- a/src/bin/proxmox-backup-manager.rs +++ b/src/bin/proxmox-backup-manager.rs @@ -355,6 +355,7 @@ fn main() { .insert("user", user_commands()) .insert("remote", remote_commands()) .insert("garbage-collection", garbage_collection_commands()) + .insert("acme", acme_mgmt_cli()) .insert("cert", cert_mgmt_cli()) .insert("subscription", subscription_commands()) .insert("sync-job", sync_job_commands()) diff --git a/src/bin/proxmox_backup_manager/acme.rs b/src/bin/proxmox_backup_manager/acme.rs new file mode 100644 index 00000000..bb8fb9b3 --- /dev/null +++ b/src/bin/proxmox_backup_manager/acme.rs @@ -0,0 +1,414 @@ +use std::io::Write; + +use anyhow::{bail, Error}; +use serde_json::Value; + +use proxmox::api::{api, cli::*, ApiHandler, RpcEnvironment}; +use proxmox::tools::fs::file_get_contents; + +use proxmox_backup::acme::AcmeClient; +use proxmox_backup::api2; +use proxmox_backup::config::acme::plugin::DnsPluginCoreUpdater; +use proxmox_backup::config::acme::{AccountName, KNOWN_ACME_DIRECTORIES}; + +pub fn acme_mgmt_cli() -> CommandLineInterface { + let cmd_def = CliCommandMap::new() + .insert("account", account_cli()) + .insert("cert", cert_cli()) + .insert("plugin", plugin_cli()); + + cmd_def.into() +} + +#[api( + input: { + properties: { + "output-format": { + schema: OUTPUT_FORMAT, + optional: true, + }, + } + } +)] +/// List acme accounts. +fn list_accounts(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> { + let output_format = get_output_format(¶m); + + let info = &api2::config::acme::API_METHOD_LIST_ACCOUNTS; + let mut data = match info.handler { + ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, + _ => unreachable!(), + }; + + let options = default_table_format_options(); + format_and_print_result_full(&mut data, &info.returns, &output_format, &options); + + Ok(()) +} + +#[api( + input: { + properties: { + name: { type: AccountName }, + "output-format": { + schema: OUTPUT_FORMAT, + optional: true, + }, + } + } +)] +/// Show acme account information. +async fn get_account(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> { + let output_format = get_output_format(¶m); + + let info = &api2::config::acme::API_METHOD_GET_ACCOUNT; + let mut data = match info.handler { + ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?, + _ => unreachable!(), + }; + + let options = default_table_format_options() + .column( + ColumnConfig::new("account") + .renderer(|value, _record| Ok(serde_json::to_string_pretty(value)?)), + ) + .column(ColumnConfig::new("directory")) + .column(ColumnConfig::new("location")) + .column(ColumnConfig::new("tos")); + format_and_print_result_full(&mut data, &info.returns, &output_format, &options); + + Ok(()) +} + +#[api( + input: { + properties: { + name: { type: AccountName }, + contact: { + description: "List of email addresses.", + }, + directory: { + type: String, + description: "The ACME Directory.", + optional: true, + }, + } + } +)] +/// Register an ACME account. +async fn register_account( + name: AccountName, + contact: String, + directory: Option, +) -> Result<(), Error> { + let directory = match directory { + Some(directory) => directory, + None => { + println!("Directory endpoints:"); + for (i, dir) in KNOWN_ACME_DIRECTORIES.iter().enumerate() { + println!("{}) {}", i, dir.url); + } + + println!("{}) Custom", KNOWN_ACME_DIRECTORIES.len()); + let mut attempt = 0; + loop { + print!("Enter selection: "); + std::io::stdout().flush()?; + + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + + match input.trim().parse::() { + Ok(n) if n < KNOWN_ACME_DIRECTORIES.len() => { + break KNOWN_ACME_DIRECTORIES[n].url.to_owned(); + } + Ok(n) if n == KNOWN_ACME_DIRECTORIES.len() => { + input.clear(); + std::io::stdin().read_line(&mut input)?; + break input.trim().to_owned(); + } + _ => eprintln!("Invalid selection."), + } + + attempt += 1; + if attempt >= 3 { + bail!("Aborting."); + } + } + } + }; + + println!("Attempting to fetch Terms of Service from {:?}", directory); + let mut client = AcmeClient::new(directory.clone()); + let tos_agreed = if let Some(tos_url) = client.terms_of_service_url().await? { + println!("Terms of Service: {}", tos_url); + print!("Do you agree to the above terms? [y|N]: "); + std::io::stdout().flush()?; + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + if input.trim().eq_ignore_ascii_case("y") { + true + } else { + false + } + } else { + false + }; + + println!("Attempting to register account with {:?}...", directory); + + let account = + api2::config::acme::do_register_account(&mut client, &name, tos_agreed, contact, None) + .await?; + + println!("Registration successful, account URL: {}", account.location); + + Ok(()) +} + +#[api( + input: { + properties: { + name: { type: AccountName }, + contact: { + description: "List of email addresses.", + type: String, + optional: true, + }, + } + } +)] +/// Update an ACME account. +async fn update_account(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> { + let info = &api2::config::acme::API_METHOD_UPDATE_ACCOUNT; + let result = match info.handler { + ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, + _ => unreachable!(), + }; + + crate::wait_for_local_worker(result.as_str().unwrap()).await?; + + Ok(()) +} + +#[api( + input: { + properties: { + name: { type: AccountName }, + force: { + description: + "Delete account data even if the server refuses to deactivate the account.", + type: Boolean, + optional: true, + default: false, + }, + } + } +)] +/// Deactivate an ACME account. +async fn deactivate_account(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> { + let info = &api2::config::acme::API_METHOD_DEACTIVATE_ACCOUNT; + let result = match info.handler { + ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, + _ => unreachable!(), + }; + + crate::wait_for_local_worker(result.as_str().unwrap()).await?; + + Ok(()) +} + +pub fn account_cli() -> CommandLineInterface { + let cmd_def = CliCommandMap::new() + .insert("list", CliCommand::new(&API_METHOD_LIST_ACCOUNTS)) + .insert( + "register", + CliCommand::new(&API_METHOD_REGISTER_ACCOUNT).arg_param(&["name", "contact"]), + ) + .insert( + "deactivate", + CliCommand::new(&API_METHOD_DEACTIVATE_ACCOUNT) + .arg_param(&["name"]) + .completion_cb("name", crate::config::acme::complete_acme_account), + ) + .insert( + "info", + CliCommand::new(&API_METHOD_GET_ACCOUNT) + .arg_param(&["name"]) + .completion_cb("name", crate::config::acme::complete_acme_account), + ) + .insert( + "update", + CliCommand::new(&API_METHOD_UPDATE_ACCOUNT) + .arg_param(&["name"]) + .completion_cb("name", crate::config::acme::complete_acme_account), + ); + + cmd_def.into() +} + +#[api( + input: { + properties: { + "output-format": { + schema: OUTPUT_FORMAT, + optional: true, + }, + } + } +)] +/// List acme plugins. +fn list_plugins(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> { + let output_format = get_output_format(¶m); + + let info = &api2::config::acme::API_METHOD_LIST_PLUGINS; + let mut data = match info.handler { + ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, + _ => unreachable!(), + }; + + let options = default_table_format_options(); + format_and_print_result_full(&mut data, &info.returns, &output_format, &options); + + Ok(()) +} + +#[api( + input: { + properties: { + id: { + type: String, + description: "Plugin ID", + }, + "output-format": { + schema: OUTPUT_FORMAT, + optional: true, + }, + } + } +)] +/// Show acme account information. +fn get_plugin(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> { + let output_format = get_output_format(¶m); + + let info = &api2::config::acme::API_METHOD_GET_PLUGIN; + let mut data = match info.handler { + ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, + _ => unreachable!(), + }; + + let options = default_table_format_options(); + format_and_print_result_full(&mut data, &info.returns, &output_format, &options); + + Ok(()) +} + +#[api( + input: { + properties: { + type: { + type: String, + description: "The ACME challenge plugin type.", + }, + core: { + type: DnsPluginCoreUpdater, + flatten: true, + }, + data: { + type: String, + description: "File containing the plugin data.", + }, + } + } +)] +/// Show acme account information. +fn add_plugin(r#type: String, core: DnsPluginCoreUpdater, data: String) -> Result<(), Error> { + let data = base64::encode(&file_get_contents(&data)?); + api2::config::acme::add_plugin(r#type, core, data)?; + Ok(()) +} + +pub fn plugin_cli() -> CommandLineInterface { + use proxmox_backup::api2::config::acme; + let cmd_def = CliCommandMap::new() + .insert("list", CliCommand::new(&API_METHOD_LIST_PLUGINS)) + .insert( + "config", // name comes from pve/pmg + CliCommand::new(&API_METHOD_GET_PLUGIN) + .arg_param(&["id"]) + .completion_cb("id", crate::config::acme::complete_acme_plugin), + ) + .insert( + "add", + CliCommand::new(&API_METHOD_ADD_PLUGIN) + .arg_param(&["type", "id"]) + .completion_cb("id", crate::config::acme::complete_acme_plugin) + .completion_cb("type", crate::config::acme::complete_acme_plugin_type), + ) + .insert( + "remove", + CliCommand::new(&acme::API_METHOD_DELETE_PLUGIN) + .arg_param(&["id"]) + .completion_cb("id", crate::config::acme::complete_acme_plugin), + ) + .insert( + "set", + CliCommand::new(&acme::API_METHOD_UPDATE_PLUGIN) + .arg_param(&["id"]) + .completion_cb("id", crate::config::acme::complete_acme_plugin), + ); + + cmd_def.into() +} + +#[api( + input: { + properties: { + force: { + description: "Force renewal even if the certificate does not expire soon.", + type: Boolean, + optional: true, + default: false, + }, + }, + }, +)] +/// Order a new ACME certificate. +async fn order_acme_cert(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> { + if !api2::node::certificates::cert_expires_soon()? { + println!("Certificate does not expire within the next 30 days, not renewing."); + return Ok(()); + } + + let info = &api2::node::certificates::API_METHOD_RENEW_ACME_CERT; + let result = match info.handler { + ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, + _ => unreachable!(), + }; + + crate::wait_for_local_worker(result.as_str().unwrap()).await?; + + Ok(()) +} + +#[api] +/// Order a new ACME certificate. +async fn revoke_acme_cert(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> { + let info = &api2::node::certificates::API_METHOD_REVOKE_ACME_CERT; + let result = match info.handler { + ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, + _ => unreachable!(), + }; + + crate::wait_for_local_worker(result.as_str().unwrap()).await?; + + Ok(()) +} + +pub fn cert_cli() -> CommandLineInterface { + let cmd_def = CliCommandMap::new() + .insert("order", CliCommand::new(&API_METHOD_ORDER_ACME_CERT)) + .insert("revoke", CliCommand::new(&API_METHOD_REVOKE_ACME_CERT)); + + cmd_def.into() +} diff --git a/src/bin/proxmox_backup_manager/mod.rs b/src/bin/proxmox_backup_manager/mod.rs index 900144aa..e574e4d4 100644 --- a/src/bin/proxmox_backup_manager/mod.rs +++ b/src/bin/proxmox_backup_manager/mod.rs @@ -1,5 +1,7 @@ mod acl; pub use acl::*; +mod acme; +pub use acme::*; mod cert; pub use cert::*; mod datastore; -- 2.20.1