]> git.proxmox.com Git - proxmox-backup.git/blame - src/config/acme/plugin.rs
add proxmox-backup-debug debian package
[proxmox-backup.git] / src / config / acme / plugin.rs
CommitLineData
cb67ecad
WB
1use anyhow::Error;
2use lazy_static::lazy_static;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use proxmox::api::{
7 api,
8 schema::*,
9 section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin},
10};
11
cb67ecad 12use crate::api2::types::PROXMOX_SAFE_ID_FORMAT;
7526d864 13use crate::backup::{open_backup_lockfile, BackupLockGuard};
cb67ecad
WB
14
15pub const PLUGIN_ID_SCHEMA: Schema = StringSchema::new("ACME Challenge Plugin ID.")
16 .format(&PROXMOX_SAFE_ID_FORMAT)
39c5db7f
DM
17 .min_length(1)
18 .max_length(32)
cb67ecad
WB
19 .schema();
20
21lazy_static! {
22 pub static ref CONFIG: SectionConfig = init();
23}
24
25#[api(
26 properties: {
27 id: { schema: PLUGIN_ID_SCHEMA },
28 },
29)]
30#[derive(Deserialize, Serialize)]
31/// Standalone ACME Plugin for the http-1 challenge.
32pub struct StandalonePlugin {
33 /// Plugin ID.
34 id: String,
35}
36
37impl Default for StandalonePlugin {
38 fn default() -> Self {
39 Self {
40 id: "standalone".to_string(),
41 }
42 }
43}
44
45#[api(
46 properties: {
47 id: { schema: PLUGIN_ID_SCHEMA },
48 disable: {
49 optional: true,
50 default: false,
51 },
52 "validation-delay": {
53 default: 30,
54 optional: true,
55 minimum: 0,
56 maximum: 2 * 24 * 60 * 60,
57 },
58 },
59)]
60/// DNS ACME Challenge Plugin core data.
61#[derive(Deserialize, Serialize, Updater)]
62#[serde(rename_all = "kebab-case")]
63pub struct DnsPluginCore {
64 /// Plugin ID.
a8a20e92
DM
65 #[updater(skip)]
66 pub id: String,
cb67ecad
WB
67
68 /// DNS API Plugin Id.
a8a20e92 69 pub api: String,
cb67ecad
WB
70
71 /// Extra delay in seconds to wait before requesting validation.
72 ///
73 /// Allows to cope with long TTL of DNS records.
74 #[serde(skip_serializing_if = "Option::is_none", default)]
a8a20e92 75 pub validation_delay: Option<u32>,
cb67ecad
WB
76
77 /// Flag to disable the config.
78 #[serde(skip_serializing_if = "Option::is_none", default)]
a8a20e92 79 pub disable: Option<bool>,
cb67ecad
WB
80}
81
82#[api(
83 properties: {
84 core: { type: DnsPluginCore },
85 },
86)]
87/// DNS ACME Challenge Plugin.
88#[derive(Deserialize, Serialize)]
89#[serde(rename_all = "kebab-case")]
90pub struct DnsPlugin {
91 #[serde(flatten)]
a8a20e92 92 pub core: DnsPluginCore,
cb67ecad 93
cb67ecad
WB
94 // We handle this property separately in the API calls.
95 /// DNS plugin data (base64url encoded without padding).
96 #[serde(with = "proxmox::tools::serde::string_as_base64url_nopad")]
a8a20e92 97 pub data: String,
cb67ecad
WB
98}
99
100impl DnsPlugin {
101 pub fn decode_data(&self, output: &mut Vec<u8>) -> Result<(), Error> {
102 Ok(base64::decode_config_buf(
103 &self.data,
104 base64::URL_SAFE_NO_PAD,
105 output,
106 )?)
107 }
108}
109
110fn init() -> SectionConfig {
111 let mut config = SectionConfig::new(&PLUGIN_ID_SCHEMA);
112
113 let standalone_schema = match &StandalonePlugin::API_SCHEMA {
114 Schema::Object(schema) => schema,
115 _ => unreachable!(),
116 };
117 let standalone_plugin = SectionConfigPlugin::new(
118 "standalone".to_string(),
119 Some("id".to_string()),
120 standalone_schema,
121 );
122 config.register_plugin(standalone_plugin);
123
124 let dns_challenge_schema = match DnsPlugin::API_SCHEMA {
125 Schema::AllOf(ref schema) => schema,
126 _ => unreachable!(),
127 };
128 let dns_challenge_plugin = SectionConfigPlugin::new(
129 "dns".to_string(),
130 Some("id".to_string()),
131 dns_challenge_schema,
132 );
133 config.register_plugin(dns_challenge_plugin);
134
135 config
136}
137
af06decd
WB
138const ACME_PLUGIN_CFG_FILENAME: &str = pbs_buildcfg::configdir!("/acme/plugins.cfg");
139const ACME_PLUGIN_CFG_LOCKFILE: &str = pbs_buildcfg::configdir!("/acme/.plugins.lck");
cb67ecad 140
7526d864 141pub fn lock() -> Result<BackupLockGuard, Error> {
cb67ecad 142 super::make_acme_dir()?;
7526d864 143 open_backup_lockfile(ACME_PLUGIN_CFG_LOCKFILE, None, true)
cb67ecad
WB
144}
145
146pub fn config() -> Result<(PluginData, [u8; 32]), Error> {
147 let content = proxmox::tools::fs::file_read_optional_string(ACME_PLUGIN_CFG_FILENAME)?
148 .unwrap_or_else(|| "".to_string());
149
150 let digest = openssl::sha::sha256(content.as_bytes());
151 let mut data = CONFIG.parse(ACME_PLUGIN_CFG_FILENAME, &content)?;
152
153 if data.sections.get("standalone").is_none() {
154 let standalone = StandalonePlugin::default();
155 data.set_data("standalone", "standalone", &standalone)
156 .unwrap();
157 }
158
159 Ok((PluginData { data }, digest))
160}
161
162pub fn save_config(config: &PluginData) -> Result<(), Error> {
163 super::make_acme_dir()?;
164 let raw = CONFIG.write(ACME_PLUGIN_CFG_FILENAME, &config.data)?;
a301c362 165 crate::backup::replace_backup_config(ACME_PLUGIN_CFG_FILENAME, raw.as_bytes())
cb67ecad
WB
166}
167
168pub struct PluginData {
169 data: SectionConfigData,
170}
171
172// And some convenience helpers.
173impl PluginData {
174 pub fn remove(&mut self, name: &str) -> Option<(String, Value)> {
175 self.data.sections.remove(name)
176 }
177
178 pub fn contains_key(&mut self, name: &str) -> bool {
179 self.data.sections.contains_key(name)
180 }
181
182 pub fn get(&self, name: &str) -> Option<&(String, Value)> {
183 self.data.sections.get(name)
184 }
185
186 pub fn get_mut(&mut self, name: &str) -> Option<&mut (String, Value)> {
187 self.data.sections.get_mut(name)
188 }
189
190 pub fn insert(&mut self, id: String, ty: String, plugin: Value) {
191 self.data.sections.insert(id, (ty, plugin));
192 }
193
194 pub fn iter(&self) -> impl Iterator<Item = (&String, &(String, Value))> + Send {
195 self.data.sections.iter()
196 }
197}