1 use std
::collections
::HashMap
;
2 use std
::ops
::ControlFlow
;
5 use anyhow
::{bail, format_err, Error}
;
8 use proxmox
::sys
::error
::SysError
;
9 use proxmox
::tools
::fs
::{CreateOptions, file_read_string}
;
11 use pbs_api_types
::PROXMOX_SAFE_ID_REGEX
;
13 use crate::api2
::types
::{
19 pub(crate) const ACME_DIR
: &str = pbs_buildcfg
::configdir
!("/acme");
20 pub(crate) const ACME_ACCOUNT_DIR
: &str = pbs_buildcfg
::configdir
!("/acme/accounts");
22 pub(crate) const ACME_DNS_SCHEMA_FN
: &str = "/usr/share/proxmox-acme/dns-challenge-schema.json";
26 // `const fn`ify this once it is supported in `proxmox`
27 fn root_only() -> CreateOptions
{
29 .owner(nix
::unistd
::ROOT
)
30 .group(nix
::unistd
::Gid
::from_raw(0))
31 .perm(nix
::sys
::stat
::Mode
::from_bits_truncate(0o700))
34 fn create_acme_subdir(dir
: &str) -> nix
::Result
<()> {
35 match proxmox
::tools
::fs
::create_dir(dir
, root_only()) {
37 Err(err
) if err
.already_exists() => Ok(()),
42 pub(crate) fn make_acme_dir() -> nix
::Result
<()> {
43 create_acme_subdir(ACME_DIR
)
46 pub(crate) fn make_acme_account_dir() -> nix
::Result
<()> {
48 create_acme_subdir(ACME_ACCOUNT_DIR
)
51 pub const KNOWN_ACME_DIRECTORIES
: &[KnownAcmeDirectory
] = &[
53 name
: "Let's Encrypt V2",
54 url
: "https://acme-v02.api.letsencrypt.org/directory",
57 name
: "Let's Encrypt V2 Staging",
58 url
: "https://acme-staging-v02.api.letsencrypt.org/directory",
62 pub const DEFAULT_ACME_DIRECTORY_ENTRY
: &KnownAcmeDirectory
= &KNOWN_ACME_DIRECTORIES
[0];
64 pub fn account_path(name
: &str) -> String
{
65 format
!("{}/{}", ACME_ACCOUNT_DIR
, name
)
69 pub fn foreach_acme_account
<F
>(mut func
: F
) -> Result
<(), Error
>
71 F
: FnMut(AcmeAccountName
) -> ControlFlow
<Result
<(), Error
>>,
73 match pbs_tools
::fs
::scan_subdir(-1, ACME_ACCOUNT_DIR
, &PROXMOX_SAFE_ID_REGEX
) {
77 let file_name
= unsafe { file.file_name_utf8_unchecked() }
;
79 if file_name
.starts_with('_'
) {
83 let account_name
= match AcmeAccountName
::from_string(file_name
.to_owned()) {
84 Ok(account_name
) => account_name
,
88 if let ControlFlow
::Break(result
) = func(account_name
) {
94 Err(err
) if err
.not_found() => Ok(()),
95 Err(err
) => Err(err
.into()),
99 pub fn mark_account_deactivated(name
: &str) -> Result
<(), Error
> {
100 let from
= account_path(name
);
102 let to
= account_path(&format
!("_deactivated_{}_{}", name
, i
));
103 if !Path
::new(&to
).exists() {
104 return std
::fs
::rename(&from
, &to
).map_err(|err
| {
106 "failed to move account path {:?} to {:?} - {}",
115 "No free slot to rename deactivated account {:?}, please cleanup {:?}",
121 pub fn load_dns_challenge_schema() -> Result
<Vec
<AcmeChallengeSchema
>, Error
> {
122 let raw
= file_read_string(&ACME_DNS_SCHEMA_FN
)?
;
123 let schemas
: serde_json
::Map
<String
, Value
> = serde_json
::from_str(&raw
)?
;
127 .map(|(id
, schema
)| AcmeChallengeSchema
{
131 .and_then(Value
::as_str
)
135 schema
: schema
.to_owned(),
140 pub fn complete_acme_account(_arg
: &str, _param
: &HashMap
<String
, String
>) -> Vec
<String
> {
141 let mut out
= Vec
::new();
142 let _
= foreach_acme_account(|name
| {
143 out
.push(name
.into_string());
144 ControlFlow
::Continue(())
149 pub fn complete_acme_plugin(_arg
: &str, _param
: &HashMap
<String
, String
>) -> Vec
<String
> {
150 match plugin
::config() {
151 Ok((config
, _digest
)) => config
153 .map(|(id
, (_type
, _cfg
))| id
.clone())
155 Err(_
) => Vec
::new(),
159 pub fn complete_acme_plugin_type(_arg
: &str, _param
: &HashMap
<String
, String
>) -> Vec
<String
> {
162 //"http".to_string(), // makes currently not realyl sense to create or the like
166 pub fn complete_acme_api_challenge_type(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
167 if param
.get("type") == Some(&"dns".to_string()) {
168 match load_dns_challenge_schema() {
169 Ok(schema
) => schema
.into_iter().map(|s
| s
.id
).collect(),
170 Err(_
) => Vec
::new(),