1 use std
::collections
::HashMap
;
4 use anyhow
::{bail, format_err, Error}
;
7 use proxmox
::sys
::error
::SysError
;
8 use proxmox
::tools
::fs
::{CreateOptions, file_read_string}
;
10 use crate::api2
::types
::{
11 PROXMOX_SAFE_ID_REGEX
,
16 use crate::tools
::ControlFlow
;
18 pub(crate) const ACME_DIR
: &str = configdir
!("/acme");
19 pub(crate) const ACME_ACCOUNT_DIR
: &str = configdir
!("/acme/accounts");
21 pub(crate) const ACME_DNS_SCHEMA_FN
: &str = "/usr/share/proxmox-acme/dns-challenge-schema.json";
25 // `const fn`ify this once it is supported in `proxmox`
26 fn root_only() -> CreateOptions
{
28 .owner(nix
::unistd
::ROOT
)
29 .group(nix
::unistd
::Gid
::from_raw(0))
30 .perm(nix
::sys
::stat
::Mode
::from_bits_truncate(0o700))
33 fn create_acme_subdir(dir
: &str) -> nix
::Result
<()> {
34 match proxmox
::tools
::fs
::create_dir(dir
, root_only()) {
36 Err(err
) if err
.already_exists() => Ok(()),
41 pub(crate) fn make_acme_dir() -> nix
::Result
<()> {
42 create_acme_subdir(ACME_DIR
)
45 pub(crate) fn make_acme_account_dir() -> nix
::Result
<()> {
47 create_acme_subdir(ACME_ACCOUNT_DIR
)
50 pub const KNOWN_ACME_DIRECTORIES
: &[KnownAcmeDirectory
] = &[
52 name
: "Let's Encrypt V2",
53 url
: "https://acme-v02.api.letsencrypt.org/directory",
56 name
: "Let's Encrypt V2 Staging",
57 url
: "https://acme-staging-v02.api.letsencrypt.org/directory",
61 pub const DEFAULT_ACME_DIRECTORY_ENTRY
: &KnownAcmeDirectory
= &KNOWN_ACME_DIRECTORIES
[0];
63 pub fn account_path(name
: &str) -> String
{
64 format
!("{}/{}", ACME_ACCOUNT_DIR
, name
)
68 pub fn foreach_acme_account
<F
>(mut func
: F
) -> Result
<(), Error
>
70 F
: FnMut(AcmeAccountName
) -> ControlFlow
<Result
<(), Error
>>,
72 match crate::tools
::fs
::scan_subdir(-1, ACME_ACCOUNT_DIR
, &PROXMOX_SAFE_ID_REGEX
) {
76 let file_name
= unsafe { file.file_name_utf8_unchecked() }
;
78 if file_name
.starts_with('_'
) {
82 let account_name
= match AcmeAccountName
::from_string(file_name
.to_owned()) {
83 Ok(account_name
) => account_name
,
87 if let ControlFlow
::Break(result
) = func(account_name
) {
93 Err(err
) if err
.not_found() => Ok(()),
94 Err(err
) => Err(err
.into()),
98 /// Run a function for each DNS plugin ID.
99 pub fn foreach_dns_plugin
<F
>(mut func
: F
) -> Result
<(), Error
>
101 F
: FnMut(&str) -> ControlFlow
<Result
<(), Error
>>,
103 match crate::tools
::fs
::read_subdir(-1, "/usr/share/proxmox-acme/dnsapi") {
105 for file
in files
.filter_map(Result
::ok
) {
106 if let Some(id
) = file
110 .and_then(|name
| name
.strip_prefix("dns_"))
111 .and_then(|name
| name
.strip_suffix(".sh"))
113 if let ControlFlow
::Break(result
) = func(id
) {
121 Err(err
) if err
.not_found() => Ok(()),
122 Err(err
) => Err(err
.into()),
126 pub fn mark_account_deactivated(name
: &str) -> Result
<(), Error
> {
127 let from
= account_path(name
);
129 let to
= account_path(&format
!("_deactivated_{}_{}", name
, i
));
130 if !Path
::new(&to
).exists() {
131 return std
::fs
::rename(&from
, &to
).map_err(|err
| {
133 "failed to move account path {:?} to {:?} - {}",
142 "No free slot to rename deactivated account {:?}, please cleanup {:?}",
148 pub fn load_dns_challenge_schema() -> Result
<Vec
<AcmeChallengeSchema
>, Error
> {
149 let raw
= file_read_string(&ACME_DNS_SCHEMA_FN
)?
;
150 let schemas
: serde_json
::Map
<String
, Value
> = serde_json
::from_str(&raw
)?
;
154 .map(|(id
, schema
)| AcmeChallengeSchema
{
158 .and_then(Value
::as_str
)
162 schema
: schema
.to_owned(),
167 pub fn complete_acme_account(_arg
: &str, _param
: &HashMap
<String
, String
>) -> Vec
<String
> {
168 let mut out
= Vec
::new();
169 let _
= foreach_acme_account(|name
| {
170 out
.push(name
.into_string());
171 ControlFlow
::CONTINUE
176 pub fn complete_acme_plugin(_arg
: &str, _param
: &HashMap
<String
, String
>) -> Vec
<String
> {
177 match plugin
::config() {
178 Ok((config
, _digest
)) => config
180 .map(|(id
, (_type
, _cfg
))| id
.clone())
182 Err(_
) => Vec
::new(),
186 pub fn complete_acme_plugin_type(_arg
: &str, _param
: &HashMap
<String
, String
>) -> Vec
<String
> {
187 vec
!["dns".to_string(), "http".to_string()]