proxmox-backup/src/bin/proxmox_backup_manager/acme.rs
Dietmar Maurer 39c5db7f0f move basic ACME types into src/api2/types/acme.rs
And rename AccountName into AcmeAccountName.
2021-05-04 11:32:18 +02:00

417 lines
12 KiB
Rust

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::api2::types::AcmeAccountName;
use proxmox_backup::config::acme::plugin::DnsPluginCoreUpdater;
use proxmox_backup::config::acme::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(&param);
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: AcmeAccountName },
"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(&param);
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: AcmeAccountName },
contact: {
description: "List of email addresses.",
},
directory: {
type: String,
description: "The ACME Directory.",
optional: true,
},
}
}
)]
/// Register an ACME account.
async fn register_account(
name: AcmeAccountName,
contact: String,
directory: Option<String>,
) -> 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::<usize>() {
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: AcmeAccountName },
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: AcmeAccountName },
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(&param);
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(&param);
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()
}