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 !param["force"].as_bool().unwrap_or(false) && !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() }