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