]> git.proxmox.com Git - proxmox-backup.git/blob - src/config/acme/mod.rs
remove pbs-tools::ops::ControlFlow
[proxmox-backup.git] / src / config / acme / mod.rs
1 use std::collections::HashMap;
2 use std::ops::ControlFlow;
3 use std::path::Path;
4
5 use anyhow::{bail, format_err, Error};
6 use serde_json::Value;
7
8 use proxmox::sys::error::SysError;
9 use proxmox::tools::fs::{CreateOptions, file_read_string};
10
11 use pbs_api_types::PROXMOX_SAFE_ID_REGEX;
12
13 use crate::api2::types::{
14 AcmeChallengeSchema,
15 KnownAcmeDirectory,
16 AcmeAccountName,
17 };
18
19 pub(crate) const ACME_DIR: &str = pbs_buildcfg::configdir!("/acme");
20 pub(crate) const ACME_ACCOUNT_DIR: &str = pbs_buildcfg::configdir!("/acme/accounts");
21
22 pub(crate) const ACME_DNS_SCHEMA_FN: &str = "/usr/share/proxmox-acme/dns-challenge-schema.json";
23
24 pub mod plugin;
25
26 // `const fn`ify this once it is supported in `proxmox`
27 fn root_only() -> CreateOptions {
28 CreateOptions::new()
29 .owner(nix::unistd::ROOT)
30 .group(nix::unistd::Gid::from_raw(0))
31 .perm(nix::sys::stat::Mode::from_bits_truncate(0o700))
32 }
33
34 fn create_acme_subdir(dir: &str) -> nix::Result<()> {
35 match proxmox::tools::fs::create_dir(dir, root_only()) {
36 Ok(()) => Ok(()),
37 Err(err) if err.already_exists() => Ok(()),
38 Err(err) => Err(err),
39 }
40 }
41
42 pub(crate) fn make_acme_dir() -> nix::Result<()> {
43 create_acme_subdir(ACME_DIR)
44 }
45
46 pub(crate) fn make_acme_account_dir() -> nix::Result<()> {
47 make_acme_dir()?;
48 create_acme_subdir(ACME_ACCOUNT_DIR)
49 }
50
51 pub const KNOWN_ACME_DIRECTORIES: &[KnownAcmeDirectory] = &[
52 KnownAcmeDirectory {
53 name: "Let's Encrypt V2",
54 url: "https://acme-v02.api.letsencrypt.org/directory",
55 },
56 KnownAcmeDirectory {
57 name: "Let's Encrypt V2 Staging",
58 url: "https://acme-staging-v02.api.letsencrypt.org/directory",
59 },
60 ];
61
62 pub const DEFAULT_ACME_DIRECTORY_ENTRY: &KnownAcmeDirectory = &KNOWN_ACME_DIRECTORIES[0];
63
64 pub fn account_path(name: &str) -> String {
65 format!("{}/{}", ACME_ACCOUNT_DIR, name)
66 }
67
68
69 pub fn foreach_acme_account<F>(mut func: F) -> Result<(), Error>
70 where
71 F: FnMut(AcmeAccountName) -> ControlFlow<Result<(), Error>>,
72 {
73 match pbs_tools::fs::scan_subdir(-1, ACME_ACCOUNT_DIR, &PROXMOX_SAFE_ID_REGEX) {
74 Ok(files) => {
75 for file in files {
76 let file = file?;
77 let file_name = unsafe { file.file_name_utf8_unchecked() };
78
79 if file_name.starts_with('_') {
80 continue;
81 }
82
83 let account_name = match AcmeAccountName::from_string(file_name.to_owned()) {
84 Ok(account_name) => account_name,
85 Err(_) => continue,
86 };
87
88 if let ControlFlow::Break(result) = func(account_name) {
89 return result;
90 }
91 }
92 Ok(())
93 }
94 Err(err) if err.not_found() => Ok(()),
95 Err(err) => Err(err.into()),
96 }
97 }
98
99 pub fn mark_account_deactivated(name: &str) -> Result<(), Error> {
100 let from = account_path(name);
101 for i in 0..100 {
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| {
105 format_err!(
106 "failed to move account path {:?} to {:?} - {}",
107 from,
108 to,
109 err
110 )
111 });
112 }
113 }
114 bail!(
115 "No free slot to rename deactivated account {:?}, please cleanup {:?}",
116 from,
117 ACME_ACCOUNT_DIR
118 );
119 }
120
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)?;
124
125 Ok(schemas
126 .iter()
127 .map(|(id, schema)| AcmeChallengeSchema {
128 id: id.to_owned(),
129 name: schema
130 .get("name")
131 .and_then(Value::as_str)
132 .unwrap_or(id)
133 .to_owned(),
134 ty: "dns",
135 schema: schema.to_owned(),
136 })
137 .collect())
138 }
139
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(())
145 });
146 out
147 }
148
149 pub fn complete_acme_plugin(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
150 match plugin::config() {
151 Ok((config, _digest)) => config
152 .iter()
153 .map(|(id, (_type, _cfg))| id.clone())
154 .collect(),
155 Err(_) => Vec::new(),
156 }
157 }
158
159 pub fn complete_acme_plugin_type(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
160 vec![
161 "dns".to_string(),
162 //"http".to_string(), // makes currently not realyl sense to create or the like
163 ]
164 }
165
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(),
171 }
172 } else {
173 Vec::new()
174 }
175 }